style(ui): polish islands and remove terminal traffic-light dots

This commit is contained in:
Mika Kuns
2026-04-21 15:56:07 +02:00
parent e6b37624a1
commit 0406d35b61
12 changed files with 330 additions and 151 deletions

View File

@@ -245,6 +245,34 @@
<Setter Property="BoxShadow" Value="0 0 0 3 #387C9166" /> <Setter Property="BoxShadow" Value="0 0 0 3 #387C9166" />
</Style> </Style>
<!-- ============================================================ -->
<!-- FLAT BUTTON — replaces the entire Button template with a -->
<!-- bare ContentPresenter so no Fluent chrome (bg / hover / -->
<!-- pressed) can render. Used to wrap TaskRowView for clicks. -->
<!-- ============================================================ -->
<Style Selector="Button.flat">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
Padding="0"
CornerRadius="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>
</Setter>
</Style>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- TASK ROW --> <!-- TASK ROW -->
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -264,12 +292,10 @@
</Setter> </Setter>
</Style> </Style>
<Style Selector="Border.task-row:pointerover"> <Style Selector="Border.task-row:pointerover">
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" /> <Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
</Style> </Style>
<Style Selector="Border.task-row.selected"> <Style Selector="Border.task-row.selected">
<Setter Property="Background" Value="{StaticResource Surface3Brush}" /> <Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
</Style> </Style>
@@ -278,7 +304,7 @@
<Setter Property="Width" Value="18" /> <Setter Property="Width" Value="18" />
<Setter Property="Height" Value="18" /> <Setter Property="Height" Value="18" />
<Setter Property="StrokeThickness" Value="1.5" /> <Setter Property="StrokeThickness" Value="1.5" />
<Setter Property="Stroke" Value="{StaticResource TextFaintBrush}" /> <Setter Property="Stroke" Value="{StaticResource TextMuteBrush}" />
<Setter Property="Fill" Value="Transparent" /> <Setter Property="Fill" Value="Transparent" />
</Style> </Style>
<Style Selector="Ellipse.task-check.done"> <Style Selector="Ellipse.task-check.done">
@@ -663,9 +689,10 @@
</Setter> </Setter>
</Style> </Style>
<!-- Selected state: shift background to accent-soft --> <!-- Selected state: rely on the left accent bar from TaskRowView;
no heavy bg or perimeter border. -->
<Style Selector="Border.task-row.selected"> <Style Selector="Border.task-row.selected">
<Setter Property="Background" Value="{StaticResource AccentSoftBrush}" /> <Setter Property="BorderBrush" Value="{StaticResource LineBrightBrush}" />
</Style> </Style>
<!-- Left accent bar for selected row --> <!-- Left accent bar for selected row -->
@@ -678,12 +705,13 @@
<Setter Property="VerticalAlignment" Value="Stretch" /> <Setter Property="VerticalAlignment" Value="Stretch" />
</Style> </Style>
<!-- Done state: dim the whole row and the title --> <!-- Done state: dim the whole row and strike through the title -->
<Style Selector="Border.task-row.done"> <Style Selector="Border.task-row.done">
<Setter Property="Opacity" Value="0.55" /> <Setter Property="Opacity" Value="0.45" />
</Style> </Style>
<Style Selector="Border.task-row.done TextBlock.task-title"> <Style Selector="Border.task-row.done TextBlock.task-title">
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" /> <Setter Property="Foreground" Value="{StaticResource TextFaintBrush}" />
<Setter Property="TextDecorations" Value="Strikethrough" />
</Style> </Style>
<!-- ============================================================ --> <!-- ============================================================ -->

View File

@@ -1,8 +1,10 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -17,7 +19,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
// Current task row (set by IslandsShellViewModel via Bind) // Current task row (set by IslandsShellViewModel via Bind)
[ObservableProperty] private TaskRowViewModel? _task; [ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
private TaskRowViewModel? _task;
// Editable fields // Editable fields
[ObservableProperty] private string _editableTitle = ""; [ObservableProperty] private string _editableTitle = "";
@@ -28,12 +32,22 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : ""; public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
// Agent strip fields // Agent strip fields
[ObservableProperty] private string _agentStatusLabel = "Idle"; [ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
private string _agentStatusLabel = "Idle";
public bool IsRunning => AgentStatusLabel == "Running"; public bool IsRunning => AgentStatusLabel == "Running";
public bool IsDone => AgentStatusLabel == "Done";
public bool IsFailed => AgentStatusLabel == "Failed";
partial void OnAgentStatusLabelChanged(string value) => OnPropertyChanged(nameof(IsRunning)); partial void OnAgentStatusLabelChanged(string value)
{
OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsDone));
OnPropertyChanged(nameof(IsFailed));
}
[ObservableProperty] private string? _model; [ObservableProperty] private string? _model;
[ObservableProperty] private string? _worktreePath; [ObservableProperty] private string? _worktreePath;
[ObservableProperty] private string? _worktreeBaseCommit;
[ObservableProperty] private string? _branchLine; [ObservableProperty] private string? _branchLine;
[ObservableProperty] private int _turns; [ObservableProperty] private int _turns;
[ObservableProperty] private int _tokens; [ObservableProperty] private int _tokens;
@@ -61,6 +75,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
public ObservableCollection<LogLineViewModel> Log { get; } = new(); public ObservableCollection<LogLineViewModel> Log { get; } = new();
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new(); public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
// Claude CLI stream-json parser + buffer for partial text deltas
private readonly StreamLineFormatter _formatter = new();
private readonly StringBuilder _claudeBuf = new();
// The task ID we are currently subscribed to for live log messages // The task ID we are currently subscribed to for live log messages
private string? _subscribedTaskId; private string? _subscribedTaskId;
@@ -89,23 +107,92 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Subscribe once; filter by current task id inside the handler // Subscribe once; filter by current task id inside the handler
_worker.TaskMessageEvent += OnTaskMessage; _worker.TaskMessageEvent += OnTaskMessage;
// Re-evaluate RunNow CanExecute when worker connection flips.
_worker.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(WorkerClient.IsConnected))
RunNowCommand.NotifyCanExecuteChanged();
};
// If the task row's live status changes (e.g. TaskStarted/Finished), mirror it.
_worker.TaskStartedEvent += (slot, taskId, startedAt) =>
{
if (Task?.Id == taskId) AgentStatusLabel = "Running";
};
_worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
{
if (Task?.Id != taskId) return;
FlushClaudeBuffer();
Log.Add(new LogLineViewModel
{
Kind = LogKind.Done,
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
});
AgentStatusLabel = status;
// Re-query to pick up worktree created during the run.
_ = RefreshWorktreeAsync(taskId);
};
_worker.WorktreeUpdatedEvent += taskId =>
{
if (Task?.Id == taskId) _ = RefreshWorktreeAsync(taskId);
};
} }
private void OnTaskMessage(string taskId, string line) private void OnTaskMessage(string taskId, string line)
{ {
if (taskId != _subscribedTaskId) return; if (taskId != _subscribedTaskId) return;
// Parse a simple prefix convention: "[sys]", "[tool]", "[claude]", etc.
// Fall back to Msg for unrecognised lines. // `[stdout] ...json...` lines are Claude CLI stream-json; parse through the
// formatter so the user sees human text, not raw JSON.
if (line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase))
{
var body = line["[stdout]".Length..].TrimStart();
var formatted = _formatter.FormatLine(body);
if (formatted is null) return; // filter noise (message_start, etc.)
AppendClaudeText(formatted);
return;
}
// Non-stdout tagged lines: flush any buffered text then classify by prefix.
FlushClaudeBuffer();
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool : line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude : line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
: line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stdout
: line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr : line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr
: line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase) ? LogKind.Done : line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase) ? LogKind.Done
: LogKind.Msg; : LogKind.Msg;
Log.Add(new LogLineViewModel { Kind = kind, Text = line }); Log.Add(new LogLineViewModel { Kind = kind, Text = line });
} }
private void AppendClaudeText(string chunk)
{
_claudeBuf.Append(chunk);
// Emit a log entry for every completed line; keep the trailing remainder buffered.
while (true)
{
var text = _claudeBuf.ToString();
var nl = text.IndexOf('\n');
if (nl < 0) break;
var piece = text[..nl].TrimEnd('\r');
if (!string.IsNullOrWhiteSpace(piece))
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
_claudeBuf.Clear();
_claudeBuf.Append(text[(nl + 1)..]);
}
}
private void FlushClaudeBuffer()
{
if (_claudeBuf.Length == 0) return;
var piece = _claudeBuf.ToString().TrimEnd();
_claudeBuf.Clear();
if (!string.IsNullOrWhiteSpace(piece))
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
}
public void Bind(TaskRowViewModel? row) public void Bind(TaskRowViewModel? row)
{ {
_loadCts?.Cancel(); _loadCts?.Cancel();
@@ -117,6 +204,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(TaskIdBadge)); OnPropertyChanged(nameof(TaskIdBadge));
Log.Clear(); Log.Clear();
Subtasks.Clear(); Subtasks.Clear();
_claudeBuf.Clear();
if (row == null) if (row == null)
{ {
@@ -137,11 +225,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
{ {
try try
{ {
await using var ctx = _dbFactory.CreateDbContext(); await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
var taskRepo = new TaskRepository(ctx);
var subtaskRepo = new SubtaskRepository(ctx); var subtaskRepo = new SubtaskRepository(ctx);
var entity = await taskRepo.GetByIdAsync(row.Id); // Own query with Include so WorktreePath/BranchLine are populated.
var entity = await ctx.Tasks
.AsNoTracking()
.Include(t => t.Worktree)
.FirstOrDefaultAsync(t => t.Id == row.Id, ct);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
if (entity == null) return; if (entity == null) return;
@@ -163,6 +254,27 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
} }
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
{
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var entity = await ctx.Tasks
.AsNoTracking()
.Include(t => t.Worktree)
.FirstOrDefaultAsync(t => t.Id == taskId);
if (entity == null || Task?.Id != taskId) return;
WorktreePath = entity.Worktree?.Path;
WorktreeBaseCommit = entity.Worktree?.BaseCommit;
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
AgentStatusLabel = entity.Status.ToString();
if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
row.DiffStat = stat;
}
catch { /* best-effort refresh */ }
}
[RelayCommand(CanExecute = nameof(CanOpenDiff))] [RelayCommand(CanExecute = nameof(CanOpenDiff))]
private async System.Threading.Tasks.Task OpenDiffAsync() private async System.Threading.Tasks.Task OpenDiffAsync()
{ {
@@ -170,6 +282,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
var diffVm = new DiffModalViewModel(_services.GetRequiredService<ClaudeDo.Data.Git.GitService>()) var diffVm = new DiffModalViewModel(_services.GetRequiredService<ClaudeDo.Data.Git.GitService>())
{ {
WorktreePath = WorktreePath, WorktreePath = WorktreePath,
BaseRef = WorktreeBaseCommit,
}; };
await diffVm.LoadAsync(); await diffVm.LoadAsync();
await ShowDiffModal(diffVm); await ShowDiffModal(diffVm);
@@ -178,19 +291,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private bool CanOpenDiff() => WorktreePath != null; private bool CanOpenDiff() => WorktreePath != null;
[RelayCommand(CanExecute = nameof(CanOpenWorktree))] [RelayCommand(CanExecute = nameof(CanOpenWorktree))]
private async System.Threading.Tasks.Task OpenWorktreeAsync() private void OpenWorktree()
{
if (WorktreePath == null || ShowWorktreeModal == null) return;
var vm = _services.GetRequiredService<WorktreeModalViewModel>();
vm.WorktreePath = WorktreePath;
await vm.LoadAsync();
await ShowWorktreeModal(vm);
}
private bool CanOpenWorktree() => WorktreePath != null;
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
private void OpenInExplorer()
{ {
if (WorktreePath == null) return; if (WorktreePath == null) return;
try try
@@ -204,11 +305,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
catch { /* explorer open is best-effort */ } catch { /* explorer open is best-effort */ }
} }
private bool CanOpenWorktree() => WorktreePath != null;
partial void OnWorktreePathChanged(string? value) partial void OnWorktreePathChanged(string? value)
{ {
OpenDiffCommand.NotifyCanExecuteChanged(); OpenDiffCommand.NotifyCanExecuteChanged();
OpenWorktreeCommand.NotifyCanExecuteChanged(); OpenWorktreeCommand.NotifyCanExecuteChanged();
OpenInExplorerCommand.NotifyCanExecuteChanged();
} }
[RelayCommand] [RelayCommand]
@@ -270,6 +372,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
if (Task == null) return; if (Task == null) return;
await _worker.CancelTaskAsync(Task.Id); await _worker.CancelTaskAsync(Task.Id);
} }
[RelayCommand(CanExecute = nameof(CanRunNow))]
private async System.Threading.Tasks.Task RunNowAsync()
{
if (Task == null) return;
AgentStatusLabel = "Running";
try
{
await _worker.RunNowAsync(Task.Id);
}
catch
{
AgentStatusLabel = "Failed";
throw;
}
}
private bool CanRunNow() =>
Task != null && _worker.IsConnected && !IsRunning;
} }
public sealed partial class SubtaskRowViewModel : ViewModelBase public sealed partial class SubtaskRowViewModel : ViewModelBase

View File

@@ -3,7 +3,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace ClaudeDo.Ui.ViewModels.Islands; namespace ClaudeDo.Ui.ViewModels.Islands;
@@ -12,11 +14,23 @@ public enum ListKind { Smart, Virtual, User }
public sealed partial class ListsIslandViewModel : ViewModelBase public sealed partial class ListsIslandViewModel : ViewModelBase
{ {
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory; private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly IServiceProvider? _services;
public event EventHandler? SelectionChanged; public event EventHandler? SelectionChanged;
public event EventHandler? FocusSearchRequested; public event EventHandler? FocusSearchRequested;
public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty); public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty);
public Func<SettingsModalViewModel, Task>? ShowSettingsModal { get; set; }
[RelayCommand]
private async Task OpenSettings()
{
if (ShowSettingsModal is null || _services is null) return;
var settingsVm = _services.GetRequiredService<SettingsModalViewModel>();
await settingsVm.LoadAsync();
await ShowSettingsModal(settingsVm);
}
public ObservableCollection<ListNavItemViewModel> Items { get; } = new(); public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new(); public ObservableCollection<ListNavItemViewModel> SmartLists { get; } = new();
public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new(); public ObservableCollection<ListNavItemViewModel> UserLists { get; } = new();
@@ -28,9 +42,10 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
public string MachineName { get; } = Environment.MachineName; public string MachineName { get; } = Environment.MachineName;
public string UserInitials { get; } public string UserInitials { get; }
public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory) public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IServiceProvider? services = null)
{ {
_dbFactory = dbFactory; _dbFactory = dbFactory;
_services = services;
var parts = Environment.UserName.Split('.', '_', '-', ' '); var parts = Environment.UserName.Split('.', '_', '-', ' ');
UserInitials = parts.Length >= 2 UserInitials = parts.Length >= 2
? $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant() ? $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant()

View File

@@ -130,10 +130,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
var running = Items.Count(i => i.Status == TaskStatus.Running); var running = Items.Count(i => i.Status == TaskStatus.Running);
var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null); var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null);
var weekday = now.ToString("dddd", CultureInfo.CurrentCulture); Subtitle = open == 1 ? "1 open task" : $"{open} open tasks";
var month = now.ToString("MMM", CultureInfo.CurrentCulture);
var day = now.Day;
Subtitle = $"{weekday}, {month} {day} · {open} open";
if (running > 0 || review > 0) if (running > 0 || review > 0)
{ {

View File

@@ -41,6 +41,7 @@ public sealed partial class DiffModalViewModel : ViewModelBase
private readonly GitService _git; private readonly GitService _git;
public required string WorktreePath { get; init; } public required string WorktreePath { get; init; }
public string? BaseRef { get; init; }
public ObservableCollection<DiffFileViewModel> Files { get; } = new(); public ObservableCollection<DiffFileViewModel> Files { get; } = new();
@@ -62,7 +63,12 @@ public sealed partial class DiffModalViewModel : ViewModelBase
Files.Clear(); Files.Clear();
string raw; string raw;
try { raw = await _git.GetDiffAsync(WorktreePath, ct); } try
{
raw = BaseRef is not null
? await _git.GetBranchDiffAsync(WorktreePath, BaseRef, ct)
: await _git.GetDiffAsync(WorktreePath, ct);
}
catch { return; } catch { return; }
if (string.IsNullOrWhiteSpace(raw)) return; if (string.IsNullOrWhiteSpace(raw)) return;

View File

@@ -42,6 +42,15 @@
<PathIcon Data="{StaticResource Icon.X}" Width="12" Height="12" <PathIcon Data="{StaticResource Icon.X}" Width="12" Height="12"
Foreground="{DynamicResource BloodBrush}"/> Foreground="{DynamicResource BloodBrush}"/>
</Button> </Button>
<!-- Hand off button — only when idle -->
<Button Grid.Column="3"
Classes="btn accent"
Content="Hand off"
Command="{Binding RunNowCommand}"
IsVisible="{Binding !IsRunning}"
ToolTip.Tip="Hand task off to Claude"
VerticalAlignment="Center"
Padding="10,4"/>
</Grid> </Grid>
<!-- Row 2: WORKTREE label + path + copy button --> <!-- Row 2: WORKTREE label + path + copy button -->
@@ -123,13 +132,14 @@
<!-- Action buttons --> <!-- Action buttons -->
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0"> <StackPanel Orientation="Horizontal" Spacing="6" Margin="0,4,0,0">
<Button Classes="btn" Content="Open diff" Command="{Binding OpenDiffCommand}"/> <Button Classes="btn" Content="Open diff" Command="{Binding OpenDiffCommand}"/>
<Button Classes="btn" Content="Worktree" Command="{Binding OpenWorktreeCommand}"/> <Button Classes="btn" Command="{Binding OpenWorktreeCommand}"
<Button Classes="btn" ToolTip.Tip="Open in file explorer" ToolTip.Tip="Open worktree in file explorer">
Command="{Binding OpenInExplorerCommand}" <StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
Padding="8,6">
<PathIcon Data="{StaticResource Icon.ArrowOut}" <PathIcon Data="{StaticResource Icon.ArrowOut}"
Width="12" Height="12" Width="11" Height="11"
Foreground="{DynamicResource TextDimBrush}"/> Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Text="Worktree" VerticalAlignment="Center"/>
</StackPanel>
</Button> </Button>
</StackPanel> </StackPanel>

View File

@@ -88,15 +88,19 @@
</Grid> </Grid>
</Border> </Border>
<!-- ── Scrollable body ── --> <!-- ── Main body: agent strip (auto) · terminal (flex) · steps+notes (auto/capped) ── -->
<ScrollViewer> <Grid RowDefinitions="Auto,*,Auto">
<StackPanel Spacing="0">
<!-- Agent strip --> <!-- Agent strip -->
<islands:AgentStripView/> <islands:AgentStripView Grid.Row="0"/>
<!-- Session terminal --> <!-- Session terminal — fills remaining vertical space -->
<islands:SessionTerminalView Height="260" Margin="0"/> <islands:SessionTerminalView Grid.Row="1" MinHeight="220" Margin="0,0,0,0"/>
<!-- Steps + Notes in a capped scroller so they never squeeze the terminal -->
<ScrollViewer Grid.Row="2" MaxHeight="240"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="0">
<!-- Subtasks section --> <!-- Subtasks section -->
<StackPanel Margin="18,12,18,0" <StackPanel Margin="18,12,18,0"
@@ -109,7 +113,6 @@
<Border Classes="subtask-row" <Border Classes="subtask-row"
Classes.done="{Binding Done}"> Classes.done="{Binding Done}">
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<!-- Ellipse checkbox -->
<Ellipse Grid.Column="0" <Ellipse Grid.Column="0"
Classes="task-check" Classes="task-check"
Classes.done="{Binding Done}" Classes.done="{Binding Done}"
@@ -149,5 +152,6 @@
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</Grid>
</DockPanel> </DockPanel>
</UserControl> </UserControl>

View File

@@ -15,10 +15,6 @@
<!-- ── Header ── --> <!-- ── Header ── -->
<Border DockPanel.Dock="Top" Classes="island-header"> <Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Margin="14,12,14,0" Spacing="4"> <StackPanel Margin="14,12,14,0" Spacing="4">
<TextBlock Classes="eyebrow"
Text="NAVIGATOR"
FontFamily="{DynamicResource MonoFont}" FontSize="9"
Foreground="{DynamicResource TextFaintBrush}" LetterSpacing="1.2"/>
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="18" <TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="18"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}" FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="Lists"/> Text="Lists"/>
@@ -69,7 +65,9 @@
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<!-- More button --> <!-- More button -->
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"> <Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"
Command="{Binding OpenSettingsCommand}"
ToolTip.Tip="Settings">
<PathIcon Data="{StaticResource Icon.MoreHorizontal}" <PathIcon Data="{StaticResource Icon.MoreHorizontal}"
Width="14" Height="14" Width="14" Height="14"
Foreground="{DynamicResource TextMuteBrush}"/> Foreground="{DynamicResource TextMuteBrush}"/>

View File

@@ -1,6 +1,8 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.Views.Modals;
namespace ClaudeDo.Ui.Views.Islands; namespace ClaudeDo.Ui.Views.Islands;
@@ -12,7 +14,10 @@ public partial class ListsIslandView : UserControl
DataContextChanged += (_, _) => DataContextChanged += (_, _) =>
{ {
if (DataContext is ListsIslandViewModel vm) if (DataContext is ListsIslandViewModel vm)
{
vm.FocusSearchRequested += (_, _) => SearchBox.Focus(); vm.FocusSearchRequested += (_, _) => SearchBox.Focus();
vm.ShowSettingsModal = ShowSettingsAsync;
}
}; };
} }
@@ -22,4 +27,12 @@ public partial class ListsIslandView : UserControl
&& DataContext is ListsIslandViewModel vm) && DataContext is ListsIslandViewModel vm)
vm.SelectCommand.Execute(item); vm.SelectCommand.Execute(item);
} }
private async System.Threading.Tasks.Task ShowSettingsAsync(SettingsModalViewModel settingsVm)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner == null) return;
var modal = new SettingsModalView { DataContext = settingsVm };
await modal.ShowDialog(owner);
}
} }

View File

@@ -10,13 +10,6 @@
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto" <Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto"
Background="{DynamicResource Surface2Brush}" Background="{DynamicResource Surface2Brush}"
Height="28"> Height="28">
<!-- Traffic-light dots -->
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4"
VerticalAlignment="Center" Margin="10,0,0,0">
<Ellipse Classes="dot-red"/>
<Ellipse Classes="dot-yellow"/>
<Ellipse Classes="dot-green"/>
</StackPanel>
<!-- Session label --> <!-- Session label -->
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Text="{Binding BranchLine, StringFormat='claude-session · {0}'}" Text="{Binding BranchLine, StringFormat='claude-session · {0}'}"
@@ -26,36 +19,34 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<!-- LIVE chip --> <!-- LIVE chip -->
<Border Grid.Column="2" Classes="live-chip" <Border Grid.Column="2" Classes="live-chip pulsing"
Classes.pulsing="{Binding IsRunning}" IsVisible="{Binding IsRunning}"
Margin="0,0,8,0" VerticalAlignment="Center"> Margin="0,0,8,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center"/> <Ellipse VerticalAlignment="Center"/>
<TextBlock Text="LIVE" VerticalAlignment="Center"/> <TextBlock Text="LIVE" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> <!-- DONE chip -->
<Border Grid.Column="2" Classes="live-chip done"
<!-- ── Prompt input (docked bottom) ── --> IsVisible="{Binding IsDone}"
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="Auto,*" Margin="0,0,8,0" VerticalAlignment="Center">
Margin="0,0,0,0" <StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
Background="{DynamicResource Surface2Brush}"> <Ellipse VerticalAlignment="Center" Fill="{DynamicResource MossBrush}"/>
<Border Grid.ColumnSpan="2" <TextBlock Text="DONE" VerticalAlignment="Center"
BorderBrush="{DynamicResource LineBrush}" Foreground="{DynamicResource MossBrush}"/>
BorderThickness="0,1,0,0"/> </StackPanel>
<TextBlock Grid.Column="0" Text="" </Border>
FontFamily="{DynamicResource MonoFont}" <!-- FAILED chip -->
FontSize="11" Foreground="{DynamicResource MossBrush}" <Border Grid.Column="2" Classes="live-chip failed"
VerticalAlignment="Center" Margin="10,0,8,0"/> IsVisible="{Binding IsFailed}"
<TextBox Grid.Column="1" Margin="0,0,8,0" VerticalAlignment="Center">
Text="{Binding PromptInput, Mode=TwoWay}" <StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
FontFamily="{DynamicResource MonoFont}" FontSize="11" <Ellipse VerticalAlignment="Center" Fill="{DynamicResource BloodBrush}"/>
Background="Transparent" BorderThickness="0" <TextBlock Text="FAILED" VerticalAlignment="Center"
Padding="0,6"> Foreground="{DynamicResource BloodBrush}"/>
<TextBox.KeyBindings> </StackPanel>
<KeyBinding Gesture="Enter" Command="{Binding SendPromptCommand}"/> </Border>
</TextBox.KeyBindings>
</TextBox>
</Grid> </Grid>
<!-- ── Log output ── --> <!-- ── Log output ── -->
@@ -74,8 +65,8 @@
Classes="log-kind" Classes="log-kind"
Tag="{Binding ClassName}" Tag="{Binding ClassName}"
Text="{Binding KindMarker}"/> Text="{Binding KindMarker}"/>
<!-- Message text — inherits terminal TextBlock color from parent selector --> <!-- Message text — selectable so the user can copy raw output -->
<TextBlock Grid.Column="2" <SelectableTextBlock Grid.Column="2"
Text="{Binding Text}" Tag="{Binding ClassName}" Text="{Binding Text}" Tag="{Binding ClassName}"
FontFamily="{DynamicResource MonoFont}" FontSize="11" FontFamily="{DynamicResource MonoFont}" FontSize="11"
Foreground="{DynamicResource TextDimBrush}" Foreground="{DynamicResource TextDimBrush}"

View File

@@ -13,7 +13,7 @@
IsVisible="{Binding IsSelected}"/> IsVisible="{Binding IsSelected}"/>
<!-- Done toggle --> <!-- Done toggle -->
<Button Grid.Column="1" Classes="check-btn" VerticalAlignment="Top" <Button Grid.Column="1" Classes="flat" VerticalAlignment="Top"
Margin="0,2,0,0" Margin="0,2,0,0"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleDoneCommand}" Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ToggleDoneCommand}"
CommandParameter="{Binding}"> CommandParameter="{Binding}">

View File

@@ -8,8 +8,7 @@
SystemDecorations="None" SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True" ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Background="Transparent" Background="{StaticResource SurfaceBrush}">
TransparencyLevelHint="AcrylicBlur">
<Window.KeyBindings> <Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/> <KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
@@ -46,13 +45,10 @@
</Style> </Style>
</Window.Styles> </Window.Styles>
<!-- Outer container --> <!-- Outer container — rectangular so the OS window rectangle stays filled (no black corners) -->
<Border CornerRadius="{StaticResource ModalCornerRadius}" <Border Background="{StaticResource SurfaceBrush}"
BoxShadow="{StaticResource ModalShadow}"
Background="{StaticResource SurfaceBrush}"
BorderBrush="{StaticResource LineBrush}" BorderBrush="{StaticResource LineBrush}"
BorderThickness="1" BorderThickness="1">
ClipToBounds="True">
<Grid RowDefinitions="36,*"> <Grid RowDefinitions="36,*">
<!-- Title bar / drag handle --> <!-- Title bar / drag handle -->