diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml
index 075892e..c308201 100644
--- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml
+++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml
@@ -245,6 +245,34 @@
+
+
+
+
+
+
+
@@ -264,12 +292,10 @@
@@ -278,7 +304,7 @@
-
+
-
+
@@ -678,12 +705,13 @@
-
+
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
index f2a2d72..8440a70 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
@@ -1,8 +1,10 @@
using System.Collections.ObjectModel;
+using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Repositories;
+using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
@@ -17,7 +19,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private readonly IServiceProvider _services;
// Current task row (set by IslandsShellViewModel via Bind)
- [ObservableProperty] private TaskRowViewModel? _task;
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
+ private TaskRowViewModel? _task;
// Editable fields
[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()}" : "";
// Agent strip fields
- [ObservableProperty] private string _agentStatusLabel = "Idle";
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
+ private string _agentStatusLabel = "Idle";
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? _worktreePath;
+ [ObservableProperty] private string? _worktreeBaseCommit;
[ObservableProperty] private string? _branchLine;
[ObservableProperty] private int _turns;
[ObservableProperty] private int _tokens;
@@ -61,6 +75,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
public ObservableCollection Log { get; } = new();
public ObservableCollection 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
private string? _subscribedTaskId;
@@ -89,23 +107,92 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Subscribe once; filter by current task id inside the handler
_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)
{
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
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
- : line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stdout
: line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr
: line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase) ? LogKind.Done
: LogKind.Msg;
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)
{
_loadCts?.Cancel();
@@ -117,6 +204,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(TaskIdBadge));
Log.Clear();
Subtasks.Clear();
+ _claudeBuf.Clear();
if (row == null)
{
@@ -137,11 +225,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
{
try
{
- await using var ctx = _dbFactory.CreateDbContext();
- var taskRepo = new TaskRepository(ctx);
+ await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
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();
if (entity == null) return;
@@ -163,6 +254,27 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
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))]
private async System.Threading.Tasks.Task OpenDiffAsync()
{
@@ -170,6 +282,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
var diffVm = new DiffModalViewModel(_services.GetRequiredService())
{
WorktreePath = WorktreePath,
+ BaseRef = WorktreeBaseCommit,
};
await diffVm.LoadAsync();
await ShowDiffModal(diffVm);
@@ -178,19 +291,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private bool CanOpenDiff() => WorktreePath != null;
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
- private async System.Threading.Tasks.Task OpenWorktreeAsync()
- {
- if (WorktreePath == null || ShowWorktreeModal == null) return;
- var vm = _services.GetRequiredService();
- vm.WorktreePath = WorktreePath;
- await vm.LoadAsync();
- await ShowWorktreeModal(vm);
- }
-
- private bool CanOpenWorktree() => WorktreePath != null;
-
- [RelayCommand(CanExecute = nameof(CanOpenWorktree))]
- private void OpenInExplorer()
+ private void OpenWorktree()
{
if (WorktreePath == null) return;
try
@@ -204,11 +305,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
catch { /* explorer open is best-effort */ }
}
+ private bool CanOpenWorktree() => WorktreePath != null;
+
partial void OnWorktreePathChanged(string? value)
{
OpenDiffCommand.NotifyCanExecuteChanged();
OpenWorktreeCommand.NotifyCanExecuteChanged();
- OpenInExplorerCommand.NotifyCanExecuteChanged();
}
[RelayCommand]
@@ -270,6 +372,25 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
if (Task == null) return;
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
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
index 19a4625..1de2f36 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
@@ -3,7 +3,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Repositories;
+using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
namespace ClaudeDo.Ui.ViewModels.Islands;
@@ -12,11 +14,23 @@ public enum ListKind { Smart, Virtual, User }
public sealed partial class ListsIslandViewModel : ViewModelBase
{
private readonly IDbContextFactory _dbFactory;
+ private readonly IServiceProvider? _services;
public event EventHandler? SelectionChanged;
public event EventHandler? FocusSearchRequested;
public void RequestFocusSearch() => FocusSearchRequested?.Invoke(this, EventArgs.Empty);
+ public Func? ShowSettingsModal { get; set; }
+
+ [RelayCommand]
+ private async Task OpenSettings()
+ {
+ if (ShowSettingsModal is null || _services is null) return;
+ var settingsVm = _services.GetRequiredService();
+ await settingsVm.LoadAsync();
+ await ShowSettingsModal(settingsVm);
+ }
+
public ObservableCollection Items { get; } = new();
public ObservableCollection SmartLists { get; } = new();
public ObservableCollection UserLists { get; } = new();
@@ -28,9 +42,10 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
public string MachineName { get; } = Environment.MachineName;
public string UserInitials { get; }
- public ListsIslandViewModel(IDbContextFactory dbFactory)
+ public ListsIslandViewModel(IDbContextFactory dbFactory, IServiceProvider? services = null)
{
_dbFactory = dbFactory;
+ _services = services;
var parts = Environment.UserName.Split('.', '_', '-', ' ');
UserInitials = parts.Length >= 2
? $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant()
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
index d96ca9c..7136260 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
@@ -130,10 +130,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
var running = Items.Count(i => i.Status == TaskStatus.Running);
var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null);
- var weekday = now.ToString("dddd", CultureInfo.CurrentCulture);
- var month = now.ToString("MMM", CultureInfo.CurrentCulture);
- var day = now.Day;
- Subtitle = $"{weekday}, {month} {day} · {open} open";
+ Subtitle = open == 1 ? "1 open task" : $"{open} open tasks";
if (running > 0 || review > 0)
{
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
index 09641ae..5924001 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
@@ -41,6 +41,7 @@ public sealed partial class DiffModalViewModel : ViewModelBase
private readonly GitService _git;
public required string WorktreePath { get; init; }
+ public string? BaseRef { get; init; }
public ObservableCollection Files { get; } = new();
@@ -62,7 +63,12 @@ public sealed partial class DiffModalViewModel : ViewModelBase
Files.Clear();
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; }
if (string.IsNullOrWhiteSpace(raw)) return;
diff --git a/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml b/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml
index 62fb214..c40a673 100644
--- a/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml
@@ -42,6 +42,15 @@
+
+
@@ -123,13 +132,14 @@
-
-
diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
index 96e7746..154cd7e 100644
--- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
@@ -88,66 +88,70 @@
-
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ Cursor="Hand"
+ Margin="0,0,8,0"/>
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml
index 16b576f..963e174 100644
--- a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml
@@ -15,10 +15,6 @@
-
@@ -69,7 +65,9 @@
-
+
diff --git a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs
index 2932838..28fbe1e 100644
--- a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs
@@ -1,6 +1,8 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using ClaudeDo.Ui.ViewModels.Islands;
+using ClaudeDo.Ui.ViewModels.Modals;
+using ClaudeDo.Ui.Views.Modals;
namespace ClaudeDo.Ui.Views.Islands;
@@ -12,7 +14,10 @@ public partial class ListsIslandView : UserControl
DataContextChanged += (_, _) =>
{
if (DataContext is ListsIslandViewModel vm)
+ {
vm.FocusSearchRequested += (_, _) => SearchBox.Focus();
+ vm.ShowSettingsModal = ShowSettingsAsync;
+ }
};
}
@@ -22,4 +27,12 @@ public partial class ListsIslandView : UserControl
&& DataContext is ListsIslandViewModel vm)
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);
+ }
}
diff --git a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml
index 7f10327..6f4eee2 100644
--- a/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml
@@ -10,13 +10,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -74,12 +65,12 @@
Classes="log-kind"
Tag="{Binding ClassName}"
Text="{Binding KindMarker}"/>
-
-
+
+
diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
index 6f3abe3..3fe4d04 100644
--- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
@@ -13,7 +13,7 @@
IsVisible="{Binding IsSelected}"/>
-
diff --git a/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml
index 8e0e1e3..dd772aa 100644
--- a/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml
+++ b/src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml
@@ -8,8 +8,7 @@
SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True"
WindowStartupLocation="CenterOwner"
- Background="Transparent"
- TransparencyLevelHint="AcrylicBlur">
+ Background="{StaticResource SurfaceBrush}">
@@ -46,13 +45,10 @@
-
-
+
+ BorderThickness="1">