feat(ui): add MarkdownView control and editable description in details island

New MarkdownView UserControl renders a markdown preview. Details island gains an editable Description section with edit/preview toggle, collapsible header, copy-to-clipboard, and debounced auto-save (400ms).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-25 09:37:14 +02:00
parent 8f94dddbc5
commit a6ca1c0108
5 changed files with 373 additions and 0 deletions

View File

@@ -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;