Files
ClaudeDo/docs/superpowers/specs/2026-04-14-ui-fixes-design.md
Mika Kuns fb3c96c405 docs(ui): add design spec for post-integration UI fixes
Covers four issues from first real test: raw NDJSON display,
missing start feedback, lost live output, and config editor gaps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:08:12 +02:00

9.5 KiB

UI Fixes Design Spec

Post-integration fixes for the Worker CLI modernization. Addresses four issues found during first real test.

Issue 1: Raw NDJSON in Live Log

Problem

TaskDetailViewModel.OnTaskMessage receives raw NDJSON lines from SignalR TaskMessage broadcasts and displays them as-is in an ItemsControl. Users see JSON like {"type":"stream_event","event":{...}} instead of readable output.

Solution: StreamLineFormatter

New stateful helper class at ClaudeDo.Ui/Helpers/StreamLineFormatter.cs.

Responsibility: Convert a single raw NDJSON line into human-readable text for display.

API:

public sealed class StreamLineFormatter
{
    // Returns formatted text to append, or null to skip the line.
    public string? FormatLine(string ndjsonLine);

    // Reads all lines from an NDJSON log file, formats each, returns complete text.
    public string FormatFile(string filePath);
}

Event mapping (moderate detail level):

NDJSON structure Display output
stream_eventcontent_block_deltatext_delta The delta text content (appended inline)
stream_eventcontent_block_starttool_use \n[Tool: {name}]\n
stream_eventcontent_block_stop \n (line separator)
stream_eventcontent_block_deltainput_json_delta null (skip — tool input noise)
stream_eventmessage_start / message_delta null (skip)
result \n--- Result ---\n{result_text}\n
system with subtype: api_retry \n[Retrying API call...]\n
assistant null (skip — content arrives via stream_events)
Malformed JSON / unknown type Raw line as-is (fallback)

State tracking: The formatter tracks whether the previous line was a text delta to avoid inserting unnecessary newlines between consecutive text chunks.

Display model change

Replace ObservableCollection<string> LiveLines with [ObservableProperty] string _liveText = "".

  • OnTaskMessage: pass line through _formatter.FormatLine(line), append result to LiveText
  • Bounding: if LiveText.Length > 50_000, trim from the front at the next newline boundary
  • View: replace ItemsControl with a read-only TextBox (AcceptsReturn="True", TextWrapping="NoWrap", monospace font)
  • Auto-scroll to bottom on text change (code-behind handler on PropertyChanged)

Rationale: Text deltas stream per-token. An ItemsControl with hundreds of tiny entries causes UI overhead. A single TextBox with appended text gives a natural terminal feel and better performance.


Issue 2: No Immediate Feedback on Task Start

Problem

After clicking RunNow, nothing happens visually until the Worker processes the request, updates the DB, and broadcasts TaskStarted. The delay (typically <1s, but noticeable) makes the app feel unresponsive.

Solution: Three-layer optimistic feedback

Layer 1 — WorkerClient local event:

Add event Action<string>? RunNowRequestedEvent to WorkerClient.

In RunNowAsync(taskId): fire RunNowRequestedEvent(taskId) before calling _hub.InvokeAsync("RunNow", taskId). This gives the UI an instant signal.

Layer 2 — TaskItemViewModel (list view):

  • Add [ObservableProperty] bool _isStarting
  • On RunNowRequestedEvent for this task: set IsStarting = true, StatusText = "starting..."
  • On TaskStartedEvent for this task: set IsStarting = false
  • RunNowCommand.CanExecute also returns false when IsStarting (prevents double-click)
  • View: RunNow button disables and shows "Starting..." state

Layer 3 — TaskDetailViewModel (detail view):

  • Subscribe to RunNowRequestedEvent → if current task, set StatusText = "starting...", clear LiveText, reset formatter
  • Subscribe to TaskStartedEvent → if current task, set StatusText = "running"
  • Both are overwritten naturally when OnTaskUpdated fires and reloads from DB

Wiring: TaskListViewModel subscribes to WorkerClient.RunNowRequestedEvent and updates the matching TaskItemViewModel. TaskDetailViewModel subscribes directly to WorkerClient events (same pattern as existing TaskMessage/TaskUpdated subscriptions).


Issue 3: Live Output Lost After Completion

Problem

LiveText is in-memory only. Once the task finishes and the user navigates away, the log content is gone. The NDJSON log file exists on disk (at task.LogPath) but is never loaded back into the UI.

Solution: Load from disk on revisit

In TaskDetailViewModel.LoadAsync, after loading the task entity:

if (task.LogPath is not null
    && task.Status is TaskStatus.Done or TaskStatus.Failed
    && File.Exists(task.LogPath))
{
    _formatter = new StreamLineFormatter();
    LiveText = _formatter.FormatFile(task.LogPath);
}

Reuses StreamLineFormatter.FormatFile from Issue 1 — no new infrastructure needed.

Edge cases:

  • Task completes while watching: LiveText already has streamed content. OnTaskUpdated triggers LoadAsync, which reloads from disk. Same content, re-parsed — no visible disruption.
  • Log file missing/deleted: File.Exists check handles it. LiveText stays empty. The "Result" field above still shows result markdown from the DB.
  • Large log files: Bounded by the same 50,000 char limit as live streaming. FormatFile applies the same trim-from-front logic.

Issue 4: Config Editors + Modal Theming

Problem A: Modal dialogs unstyled

ListEditorView and TaskEditorView are <Window> elements with no explicit background. They render as black with white text, not matching the app's green-accented dark theme defined in App.axaml.

Fix: Apply app resource brushes

Both editor <Window> elements get:

  • Background="{StaticResource WindowBgBrush}" (#1c1e21)
  • Label TextBlocks: Foreground="{StaticResource TextSecondaryBrush}"
  • Save button: Background="{StaticResource AccentBrush}" (green accent #3d9474)
  • Cancel button: default Fluent dark theme (correct once window bg is set)

Problem B: No UI for model/prompt/agent config

The backend supports per-list config (list_config table) and per-task overrides (tasks.model, tasks.system_prompt, tasks.agent_path), but there are no editor fields for these.

Solution: Extend existing editors

Model selection

ComboBox shows short display labels, mapped to actual model IDs:

Display Model ID
Sonnet claude-sonnet-4-6
Opus claude-opus-4-6
Haiku claude-haiku-4-5

Default model: Sonnet (claude-sonnet-4-6). Applied in TaskRunner config resolution as the final fallback when both task and list config have no model set.

WorkerClient — add GetAgents

New methods on WorkerClient:

  • Task<List<AgentInfo>> GetAgentsAsync() — calls hub GetAgents()
  • Task RefreshAgentsAsync() — calls hub RefreshAgents()
  • record AgentInfo(string Name, string Description, string Path) — DTO

ListEditorViewModel extensions

Three new properties:

  • [ObservableProperty] string _model — ComboBox: Sonnet (default for new lists), Opus, Haiku
  • [ObservableProperty] string? _systemPrompt — TextBox, multiline, optional
  • [ObservableProperty] string? _agentPath — ComboBox populated from GetAgentsAsync(), empty = none

InitForEdit loads existing config via ListRepository.GetConfigAsync(). Save persists via ListRepository.SetConfigAsync().

TaskEditorViewModel extensions

Same three fields, but with inheritance indicators:

  • Model ComboBox: first option "(list default)" → maps to null (inherit from list config, which falls back to Sonnet)
  • SystemPrompt: placeholder text "(inherits from list)"
  • AgentPath ComboBox: first option "(list default)" → maps to null

InitForEdit reads from TaskEntity.Model/SystemPrompt/AgentPath. Save writes them back to the entity.

View layout

Both editors add an "Agent Config" section below existing fields, separated by a horizontal divider line. Contains: Model dropdown, System Prompt text area, Agent File picker. Always visible (no collapse — only three fields).

Window heights increase to accommodate new fields:

  • ListEditorView: 280 → ~450
  • TaskEditorView: 420 → ~580

Out of Scope

  • Agent file creation/editing UI (agents are managed as .md files on disk; editors only pick from existing agents)
  • Token usage display in live output
  • Run history viewer (multiple runs per task)
  • Rich text rendering (markdown in result/output)

Files Changed

New

  • src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs

Modified

  • src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs — LiveText, formatter, start feedback, log reload
  • src/ClaudeDo.Ui/Views/TaskDetailView.axaml — TextBox replaces ItemsControl, auto-scroll
  • src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs — auto-scroll handler (if needed)
  • src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs — IsStarting property
  • src/ClaudeDo.Ui/Views/TaskListView.axaml — starting state visual
  • src/ClaudeDo.Ui/Services/WorkerClient.cs — RunNowRequestedEvent, GetAgentsAsync, RefreshAgentsAsync, AgentInfo DTO
  • src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs — config fields, agent loading
  • src/ClaudeDo.Ui/Views/ListEditorView.axaml — config section, theming
  • src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs — config override fields, agent loading
  • src/ClaudeDo.Ui/Views/TaskEditorView.axaml — config section, theming
  • src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs — wire RunNowRequestedEvent to TaskItemViewModels
  • src/ClaudeDo.Worker/Runner/TaskRunner.cs — default model fallback to claude-sonnet-4-6