diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
index 5aa5e16..5fc5e78 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
@@ -26,9 +26,33 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Editable fields
[ObservableProperty] private string _editableTitle = "";
+ [ObservableProperty] private string _editableDescription = "";
+ [ObservableProperty] private bool _isEditingDescription;
+ [ObservableProperty] private bool _isDescriptionExpanded = true;
[ObservableProperty] private string _notes = "";
[ObservableProperty] private string _promptInput = "";
+ public bool IsDescriptionEditorVisible => IsDescriptionExpanded && IsEditingDescription;
+ public bool IsDescriptionPreviewVisible => IsDescriptionExpanded && !IsEditingDescription;
+
+ partial void OnIsDescriptionExpandedChanged(bool value)
+ {
+ OnPropertyChanged(nameof(IsDescriptionEditorVisible));
+ OnPropertyChanged(nameof(IsDescriptionPreviewVisible));
+ }
+
+ partial void OnIsEditingDescriptionChanged(bool value)
+ {
+ OnPropertyChanged(nameof(IsDescriptionEditorVisible));
+ OnPropertyChanged(nameof(IsDescriptionPreviewVisible));
+ }
+
+ [RelayCommand]
+ private void ToggleEditDescription() => IsEditingDescription = !IsEditingDescription;
+
+ [RelayCommand]
+ private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;
+
// Short task-id badge, e.g. "#T1A"
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
@@ -78,6 +102,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private bool _suppressAgentSave;
private CancellationTokenSource? _agentSaveCts;
+ private bool _suppressDescSave;
+ private CancellationTokenSource? _descSaveCts;
+
public bool IsAgentSectionEnabled => !IsRunning;
[ObservableProperty] private string? _worktreePath;
@@ -269,6 +296,31 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();
+ partial void OnEditableDescriptionChanged(string value)
+ {
+ if (_suppressDescSave || Task is null) return;
+ _descSaveCts?.Cancel();
+ _descSaveCts = new CancellationTokenSource();
+ _ = SaveDescriptionAsync(_descSaveCts.Token);
+ }
+
+ private async System.Threading.Tasks.Task SaveDescriptionAsync(CancellationToken ct)
+ {
+ try
+ {
+ await System.Threading.Tasks.Task.Delay(400, ct);
+ if (Task is null) return;
+ await using var ctx = _dbFactory.CreateDbContext();
+ var repo = new TaskRepository(ctx);
+ var entity = await repo.GetByIdAsync(Task.Id);
+ if (entity is null) return;
+ entity.Description = string.IsNullOrWhiteSpace(EditableDescription) ? null : EditableDescription;
+ await repo.UpdateAsync(entity);
+ }
+ catch (OperationCanceledException) { }
+ catch { }
+ }
+
private void QueueAgentSave()
{
if (_suppressAgentSave || Task is null) return;
@@ -348,6 +400,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
{
_subscribedTaskId = null;
EditableTitle = "";
+ EditableDescription = "";
Notes = "";
Model = null;
WorktreePath = null;
@@ -392,6 +445,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
if (entity == null) return;
EditableTitle = entity.Title;
+ _suppressDescSave = true;
+ try { EditableDescription = entity.Description ?? ""; }
+ finally { _suppressDescSave = false; }
Notes = entity.Notes ?? "";
Model = entity.Model;
WorktreePath = entity.Worktree?.Path;
diff --git a/src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml b/src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml
new file mode 100644
index 0000000..ae7d4ef
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml.cs b/src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml.cs
new file mode 100644
index 0000000..08a218d
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml.cs
@@ -0,0 +1,231 @@
+using System.Text.RegularExpressions;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+
+namespace ClaudeDo.Ui.Views.Controls;
+
+public partial class MarkdownView : UserControl
+{
+ public static readonly StyledProperty MarkdownProperty =
+ AvaloniaProperty.Register(nameof(Markdown));
+
+ public string? Markdown
+ {
+ get => GetValue(MarkdownProperty);
+ set => SetValue(MarkdownProperty, value);
+ }
+
+ private StackPanel? _host;
+
+ public MarkdownView()
+ {
+ InitializeComponent();
+ _host = this.FindControl("Host");
+ }
+
+ private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == MarkdownProperty)
+ Render(change.GetNewValue());
+ }
+
+ private static readonly Regex NumberedItem = new(@"^\s*(\d+)\.\s+(.*)$", RegexOptions.Compiled);
+
+ private void Render(string? md)
+ {
+ _host ??= this.FindControl("Host");
+ if (_host is null) return;
+ _host.Children.Clear();
+
+ if (string.IsNullOrWhiteSpace(md))
+ return;
+
+ var lines = md.Replace("\r\n", "\n").Split('\n');
+ int i = 0;
+ while (i < lines.Length)
+ {
+ var line = lines[i];
+ if (string.IsNullOrWhiteSpace(line)) { i++; continue; }
+
+ if (line.StartsWith("### "))
+ {
+ _host.Children.Add(Heading(line[4..], 13));
+ i++;
+ continue;
+ }
+ if (line.StartsWith("## "))
+ {
+ _host.Children.Add(Heading(line[3..], 15));
+ i++;
+ continue;
+ }
+ if (line.StartsWith("# "))
+ {
+ _host.Children.Add(Heading(line[2..], 17));
+ i++;
+ continue;
+ }
+
+ if (line.StartsWith("- ") || line.StartsWith("* "))
+ {
+ while (i < lines.Length && (lines[i].StartsWith("- ") || lines[i].StartsWith("* ")))
+ {
+ _host.Children.Add(Bullet(lines[i][2..]));
+ i++;
+ }
+ continue;
+ }
+
+ var num = NumberedItem.Match(line);
+ if (num.Success)
+ {
+ while (i < lines.Length && NumberedItem.IsMatch(lines[i]))
+ {
+ var m = NumberedItem.Match(lines[i]);
+ _host.Children.Add(Numbered(m.Groups[1].Value, m.Groups[2].Value));
+ i++;
+ }
+ continue;
+ }
+
+ var paragraph = new System.Text.StringBuilder();
+ while (i < lines.Length
+ && !string.IsNullOrWhiteSpace(lines[i])
+ && !IsBlockStart(lines[i]))
+ {
+ if (paragraph.Length > 0) paragraph.Append(' ');
+ paragraph.Append(lines[i].Trim());
+ i++;
+ }
+ _host.Children.Add(Paragraph(paragraph.ToString()));
+ }
+ }
+
+ private static bool IsBlockStart(string line) =>
+ line.StartsWith("# ") || line.StartsWith("## ") || line.StartsWith("### ")
+ || line.StartsWith("- ") || line.StartsWith("* ")
+ || NumberedItem.IsMatch(line);
+
+ private static Control Heading(string text, double size)
+ {
+ var tb = new SelectableTextBlock
+ {
+ FontSize = size,
+ FontWeight = FontWeight.SemiBold,
+ TextWrapping = TextWrapping.Wrap,
+ Margin = new Thickness(0, 4, 0, 2),
+ };
+ AppendInlines(tb.Inlines!, text);
+ return tb;
+ }
+
+ private static Control Paragraph(string text)
+ {
+ var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
+ AppendInlines(tb.Inlines!, text);
+ return tb;
+ }
+
+ private static Control Bullet(string text)
+ {
+ var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("14,*") };
+ var dot = new TextBlock
+ {
+ Text = "•",
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(0, 0, 4, 0),
+ };
+ Grid.SetColumn(dot, 0);
+ grid.Children.Add(dot);
+
+ var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
+ AppendInlines(tb.Inlines!, text);
+ Grid.SetColumn(tb, 1);
+ grid.Children.Add(tb);
+ return grid;
+ }
+
+ private static Control Numbered(string num, string text)
+ {
+ var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("20,*") };
+ var lbl = new TextBlock
+ {
+ Text = $"{num}.",
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(0, 0, 4, 0),
+ };
+ Grid.SetColumn(lbl, 0);
+ grid.Children.Add(lbl);
+
+ var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
+ AppendInlines(tb.Inlines!, text);
+ Grid.SetColumn(tb, 1);
+ grid.Children.Add(tb);
+ return grid;
+ }
+
+ private static void AppendInlines(InlineCollection inlines, string text)
+ {
+ int i = 0;
+ var plain = new System.Text.StringBuilder();
+
+ void FlushPlain()
+ {
+ if (plain.Length == 0) return;
+ inlines.Add(new Run(plain.ToString()));
+ plain.Clear();
+ }
+
+ while (i < text.Length)
+ {
+ if (i + 1 < text.Length && text[i] == '*' && text[i + 1] == '*')
+ {
+ int close = text.IndexOf("**", i + 2, System.StringComparison.Ordinal);
+ if (close > i + 2)
+ {
+ FlushPlain();
+ inlines.Add(new Run(text[(i + 2)..close]) { FontWeight = FontWeight.Bold });
+ i = close + 2;
+ continue;
+ }
+ }
+ if (text[i] == '*')
+ {
+ int close = text.IndexOf('*', i + 1);
+ if (close > i + 1)
+ {
+ FlushPlain();
+ inlines.Add(new Run(text[(i + 1)..close]) { FontStyle = FontStyle.Italic });
+ i = close + 1;
+ continue;
+ }
+ }
+ if (text[i] == '`')
+ {
+ int close = text.IndexOf('`', i + 1);
+ if (close > i + 1)
+ {
+ FlushPlain();
+ inlines.Add(new Run(text[(i + 1)..close])
+ {
+ FontFamily = new FontFamily("Consolas,Menlo,monospace"),
+ Background = new SolidColorBrush(Color.FromArgb(60, 127, 127, 127)),
+ });
+ i = close + 1;
+ continue;
+ }
+ }
+
+ plain.Append(text[i]);
+ i++;
+ }
+ FlushPlain();
+ }
+}
diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
index 608d81d..08dc8f6 100644
--- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
+ xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Islands.DetailsIslandView"
x:DataType="vm:DetailsIslandViewModel">
@@ -214,6 +215,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs
index 9b2816f..923d4cc 100644
--- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml.cs
@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
@@ -147,4 +148,12 @@ public partial class DetailsIslandView : UserControl
if (DataContext is DetailsIslandViewModel vm)
vm.SaveNotesCommand.Execute(null);
}
+
+ private async void OnCopyDescriptionClick(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not DetailsIslandViewModel vm) return;
+ var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+ if (clipboard is null) return;
+ await clipboard.SetTextAsync(vm.EditableDescription ?? string.Empty);
+ }
}