From 63fdf14eba26c11c59d52729343eb440223f785c Mon Sep 17 00:00:00 2001 From: Jesse Liberty Date: Sun, 20 Sep 2026 13:31:25 -0400 Subject: [PATCH] feat: Implement Revision Status Controls feature - Added tasks for the List Launcher Workflow, including setup, foundational workspace contract, and user stories for launching saved sessions and compact command controls. - Created specification quality checklist, UI contract, data model, implementation plan, quickstart guide, research documentation, and feature specification for Revision Status Controls. - Defined requirements for enabling revision requests, displaying the latest status, and maintaining a compact command bar layout. - Ensured all changes adhere to accessibility standards and responsive design principles. Feature 6 complete Feature 7 ready for tasks --- .gitignore | 2 + BlogWriter.Tests/SessionListSelectionTests.cs | 35 +++ .../BlogWorkspaceServiceTests.cs | 45 +++- BlogWriter.Web.Tests/CommandBarTests.cs | 36 ++++ BlogWriter.Web.Tests/HelpDialogTestHelpers.cs | 14 ++ BlogWriter.Web.Tests/HomePageTests.cs | 14 +- .../ListLauncherTestHelpers.cs | 26 +++ BlogWriter.Web.Tests/RunCommandDialogTests.cs | 45 ++++ BlogWriter.Web.Tests/WorkflowLogTests.cs | 14 ++ BlogWriter.Web.Tests/WorkspaceBrowserTests.cs | 76 ++++++- BlogWriter.Web/Components/CommandBar.razor | 40 +++- BlogWriter.Web/Components/Pages/Home.razor | 27 ++- .../Components/RunCommandDialog.razor | 69 ++++++ .../Services/BlogWorkspaceService.cs | 58 +++++ BlogWriter.Web/Services/BlogWorkspaceState.cs | 3 +- BlogWriter.Web/wwwroot/app.css | 15 +- README.md | 4 +- docs/configuration.md | 5 + docs/~configuration.md.saved.bak | 112 ++++++++++ .../checklists/requirements.md | 36 ++++ .../contracts/list-launcher-ui.md | 44 ++++ .../006-list-launcher-workflow/data-model.md | 62 ++++++ specs/006-list-launcher-workflow/plan.md | 125 +++++++++++ .../006-list-launcher-workflow/quickstart.md | 72 +++++++ specs/006-list-launcher-workflow/research.md | 65 ++++++ specs/006-list-launcher-workflow/spec.md | 134 ++++++++++++ specs/006-list-launcher-workflow/tasks.md | 199 ++++++++++++++++++ .../checklists/requirements.md | 36 ++++ .../contracts/revision-status-ui.md | 26 +++ .../data-model.md | 52 +++++ specs/007-revision-status-controls/plan.md | 110 ++++++++++ .../quickstart.md | 55 +++++ .../007-revision-status-controls/research.md | 41 ++++ specs/007-revision-status-controls/spec.md | 117 ++++++++++ 34 files changed, 1787 insertions(+), 27 deletions(-) create mode 100644 BlogWriter.Tests/SessionListSelectionTests.cs create mode 100644 BlogWriter.Web.Tests/CommandBarTests.cs create mode 100644 BlogWriter.Web.Tests/HelpDialogTestHelpers.cs create mode 100644 BlogWriter.Web.Tests/ListLauncherTestHelpers.cs create mode 100644 BlogWriter.Web.Tests/RunCommandDialogTests.cs create mode 100644 BlogWriter.Web/Components/RunCommandDialog.razor create mode 100644 docs/~configuration.md.saved.bak create mode 100644 specs/006-list-launcher-workflow/checklists/requirements.md create mode 100644 specs/006-list-launcher-workflow/contracts/list-launcher-ui.md create mode 100644 specs/006-list-launcher-workflow/data-model.md create mode 100644 specs/006-list-launcher-workflow/plan.md create mode 100644 specs/006-list-launcher-workflow/quickstart.md create mode 100644 specs/006-list-launcher-workflow/research.md create mode 100644 specs/006-list-launcher-workflow/spec.md create mode 100644 specs/006-list-launcher-workflow/tasks.md create mode 100644 specs/007-revision-status-controls/checklists/requirements.md create mode 100644 specs/007-revision-status-controls/contracts/revision-status-ui.md create mode 100644 specs/007-revision-status-controls/data-model.md create mode 100644 specs/007-revision-status-controls/plan.md create mode 100644 specs/007-revision-status-controls/quickstart.md create mode 100644 specs/007-revision-status-controls/research.md create mode 100644 specs/007-revision-status-controls/spec.md diff --git a/.gitignore b/.gitignore index 901c8af..76b4210 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ config.json .env .env.* !.env.example +bin/ +obj/ .claude/ .azure *.user diff --git a/BlogWriter.Tests/SessionListSelectionTests.cs b/BlogWriter.Tests/SessionListSelectionTests.cs new file mode 100644 index 0000000..5a7045b --- /dev/null +++ b/BlogWriter.Tests/SessionListSelectionTests.cs @@ -0,0 +1,35 @@ +using Xunit; + +namespace BlogWriter.Tests; + +public sealed class SessionListSelectionTests +{ + [Theory] + [InlineData("1", 1)] + [InlineData(" 2 ", 2)] + public void TryResolve_UsesOneBasedDisplayedPosition(string input, int expectedIndex) + { + IReadOnlyList sessions = [CreateSummary("one"), CreateSummary("two")]; + + Assert.True(SessionListSelection.TryResolve(input, sessions, out BlogSessionSummary? selected)); + Assert.Equal(sessions[expectedIndex - 1].Id, selected!.Id); + } + + [Theory] + [InlineData("")] + [InlineData("0")] + [InlineData("-1")] + [InlineData("1.5")] + [InlineData("text")] + [InlineData("3")] + public void TryResolve_RejectsInvalidOrOutOfRangeInput(string input) + { + IReadOnlyList sessions = [CreateSummary("one"), CreateSummary("two")]; + + Assert.False(SessionListSelection.TryResolve(input, sessions, out BlogSessionSummary? selected)); + Assert.Null(selected); + } + + private static BlogSessionSummary CreateSummary(string task) => + new(Guid.NewGuid().ToString("N"), task, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow); +} diff --git a/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs b/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs index abc1baa..4a04f12 100644 --- a/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs +++ b/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs @@ -31,6 +31,47 @@ public async Task SubmitInitialAsync_PublishesCompletedDraftAndReview() Assert.Equal(1, sessions.StartCalls); } + [Fact] + public async Task LaunchSelectionAsync_RestoresPromptContextAndStartsExactlyOnce() + { + var sessions = new StubSessionService + { + Summaries = [CreateSummary("saved")], + SessionToLoad = ListLauncherTestHelpers.Session("saved topic", "tighten the ending"), + }; + var workspace = new BlogWorkspaceService(sessions, TimeSpan.FromMilliseconds(25)); + workspace.State.Draft = "old draft"; + workspace.State.Review = "old review"; + workspace.State.InitialPrompt = "old prompt"; + workspace.State.RevisionPrompt = "old revision"; + + await workspace.ListAsync(true); + await workspace.LaunchSelectionAsync("1"); + + Assert.Equal(1, sessions.StartCalls); + Assert.Equal("saved topic", sessions.LastStartPrompt); + Assert.Equal("tighten the ending", workspace.State.RevisionPrompt); + Assert.Equal(WorkspaceMode.Draft, workspace.State.Mode); + Assert.Empty(workspace.State.SelectionError ?? ""); + } + + [Theory] + [InlineData("")] + [InlineData("0")] + [InlineData("9")] + public async Task LaunchSelectionAsync_InvalidInputDoesNotLoadOrStart(string input) + { + var sessions = new StubSessionService { Summaries = [CreateSummary("saved")] }; + var workspace = new BlogWorkspaceService(sessions, TimeSpan.FromMilliseconds(25)); + await workspace.ListAsync(false); + + await workspace.LaunchSelectionAsync(input); + + Assert.Equal(0, sessions.LoadCalls); + Assert.Equal(0, sessions.StartCalls); + Assert.NotNull(workspace.State.SelectionError); + } + [Fact] public async Task SubmitInitialAsync_AppendsLifecycleAndReviewerOutput() { @@ -101,7 +142,7 @@ public async Task DuplicateReviewerOutputIsRenderedOnce() await workspace.SubmitInitialAsync(); sessions.LastOutput!.Report(BlogWorkspaceOutputTestHelpers.Review("review", "initial-review")); - Assert.Equal(1, workspace.State.Review.Split("\n\n", StringSplitOptions.None).Length); + Assert.Single(workspace.State.Review.Split("\n\n", StringSplitOptions.None)); } [Fact] @@ -398,6 +439,7 @@ private static BlogSession CreateSession( private sealed class StubSessionService : IBlogWriterSessionService { public int StartCalls { get; private set; } + public string? LastStartPrompt { get; private set; } public int LoadCalls { get; private set; } public WordRange? LastStartRange { get; private set; } public WordRange? LastRevisionRange { get; private set; } @@ -410,6 +452,7 @@ private sealed class StubSessionService : IBlogWriterSessionService public Task StartAsync(string prompt, int minWords = ResearchState.DefaultMinWords, int maxWords = ResearchState.DefaultMaxWords, CancellationToken cancellationToken = default, IProgress? output = null) { StartCalls++; + LastStartPrompt = prompt; LastOutput = output; output?.Report(WorkflowOutputUpdate.Create( WorkflowOutputKind.ReviewerFeedback, diff --git a/BlogWriter.Web.Tests/CommandBarTests.cs b/BlogWriter.Web.Tests/CommandBarTests.cs new file mode 100644 index 0000000..9124d95 --- /dev/null +++ b/BlogWriter.Web.Tests/CommandBarTests.cs @@ -0,0 +1,36 @@ +using Bunit; +using BlogWriter.Web.Components; + +namespace BlogWriter.Web.Tests; + +public sealed class CommandBarTests : BunitContext +{ + public CommandBarTests() => JSInterop.Mode = JSRuntimeMode.Loose; + + [Fact] + public void CommandBar_ShowsInlineThreeDigitSelectorAndHelp() + { + IRenderedComponent cut = Render(parameters => parameters + .Add(component => component.SelectionVisible, true) + .Add(component => component.SelectionInput, "12") + .Add(component => component.ReviseEnabled, true)); + + Assert.Equal("12", cut.Find("#command-session-number").GetAttribute("value")); + Assert.Equal("3", cut.Find("#command-session-number").GetAttribute("maxlength")); + Assert.Equal("Help", cut.Find("button[data-command='help']").GetAttribute("aria-label")); + Assert.DoesNotContain("session-selector", cut.Markup); + } + + [Fact] + public void CommandBar_SelectionChangeInvokesCallback() + { + string? value = null; + IRenderedComponent cut = Render(parameters => parameters + .Add(component => component.SelectionVisible, true) + .Add(component => component.SelectionChanged, (string selected) => value = selected)); + + cut.Find("#command-session-number").Change("2"); + + Assert.Equal("2", value); + } +} diff --git a/BlogWriter.Web.Tests/HelpDialogTestHelpers.cs b/BlogWriter.Web.Tests/HelpDialogTestHelpers.cs new file mode 100644 index 0000000..dcf70a7 --- /dev/null +++ b/BlogWriter.Web.Tests/HelpDialogTestHelpers.cs @@ -0,0 +1,14 @@ +using Bunit; +using Microsoft.JSInterop; + +namespace BlogWriter.Web.Tests; + +internal static class HelpDialogTestHelpers +{ + public static void AllowDialogOpen(BunitContext context) => + context.JSInterop.SetupVoid("blogWriterDialog.show", _ => true); + + public static void FailClipboardCopy(BunitContext context) => + context.JSInterop.SetupVoid("navigator.clipboard.writeText", _ => true) + .SetException(new JSException("Clipboard permission denied.")); +} diff --git a/BlogWriter.Web.Tests/HomePageTests.cs b/BlogWriter.Web.Tests/HomePageTests.cs index 88c8f3d..dc0f1e8 100644 --- a/BlogWriter.Web.Tests/HomePageTests.cs +++ b/BlogWriter.Web.Tests/HomePageTests.cs @@ -11,7 +11,7 @@ public sealed class HomePageTests : BunitContext public HomePageTests() => JSInterop.Mode = JSRuntimeMode.Loose; [Fact] - public void Home_RendersWritingWorkspaceAndFourCommands() + public void Home_RendersWritingWorkspaceAndFiveCommands() { BlogWorkspaceService workspace = RegisterWorkspace(); @@ -21,7 +21,7 @@ public void Home_RendersWritingWorkspaceAndFourCommands() Assert.NotNull(cut.Find("#revision-prompt")); Assert.NotNull(cut.Find("[aria-labelledby='draft-heading']")); Assert.NotNull(cut.Find("[aria-labelledby='review-heading']")); - Assert.Equal(["New", "List", "Revise", "Quit"], + Assert.Equal(["New", "List", "Revise", "Quit", "?"], cut.FindAll(".command-bar button").Select(button => button.TextContent.Trim()).ToArray()); Assert.True(cut.Find("button[data-command='revise']").HasAttribute("disabled")); Assert.False(workspace.State.IsSelectionVisible); @@ -53,8 +53,9 @@ public void Home_ShowsNumberInputAndEnablesReviseForNonEmptyList() cut.Find("button[data-command='list']").Click(); cut.WaitForAssertion(() => { - Assert.NotNull(cut.Find("#session-number")); + Assert.NotNull(cut.Find("#command-session-number")); Assert.False(cut.Find("button[data-command='revise']").HasAttribute("disabled")); + Assert.False(cut.Find("#revision-prompt").HasAttribute("disabled")); Assert.Contains("[1]", cut.Find(".session-list").TextContent); }); } @@ -88,6 +89,7 @@ public void Home_KeepsNewListAndQuitEnabledWhileReviseIsConditional() Assert.False(cut.Find("button[data-command='list']").HasAttribute("disabled")); Assert.False(cut.Find("button[data-command='quit']").HasAttribute("disabled")); Assert.True(cut.Find("button[data-command='revise']").HasAttribute("disabled")); + Assert.True(cut.Find("#revision-prompt").HasAttribute("disabled")); } [Fact] @@ -151,11 +153,9 @@ public void Home_ValidSelectionLoadsSavedSession() ]); IRenderedComponent cut = Render(); cut.Find("button[data-command='list']").Click(); - cut.WaitForElement("#session-number").Input("1"); + cut.WaitForElement("#command-session-number").Change("1"); - cut.Find("button[data-command='revise']").Click(); - - cut.WaitForAssertion(() => Assert.Equal("loaded", workspace.State.Draft)); + cut.WaitForAssertion(() => Assert.Equal("draft", workspace.State.Draft)); } [Fact] diff --git a/BlogWriter.Web.Tests/ListLauncherTestHelpers.cs b/BlogWriter.Web.Tests/ListLauncherTestHelpers.cs new file mode 100644 index 0000000..d2316eb --- /dev/null +++ b/BlogWriter.Web.Tests/ListLauncherTestHelpers.cs @@ -0,0 +1,26 @@ +namespace BlogWriter.Web.Tests; + +internal static class ListLauncherTestHelpers +{ + public static BlogSessionSummary Summary(string task) => + new(Guid.NewGuid().ToString("N"), task, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow); + + public static BlogSession Session( + string mainTask, + string currentSubTask = "", + string draft = "draft", + string review = "review") => new() + { + Id = Guid.NewGuid().ToString("N"), + OwnerId = "owner", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + State = new ResearchState + { + MainTask = mainTask, + CurrentSubTask = currentSubTask, + Draft = draft, + ReviewNotes = review, + }, + }; +} diff --git a/BlogWriter.Web.Tests/RunCommandDialogTests.cs b/BlogWriter.Web.Tests/RunCommandDialogTests.cs new file mode 100644 index 0000000..a2e6949 --- /dev/null +++ b/BlogWriter.Web.Tests/RunCommandDialogTests.cs @@ -0,0 +1,45 @@ +using Bunit; +using BlogWriter.Web.Components; + +namespace BlogWriter.Web.Tests; + +public sealed class RunCommandDialogTests : BunitContext +{ + public RunCommandDialogTests() => JSInterop.Mode = JSRuntimeMode.Loose; + + [Fact] + public void Dialog_ShowsExactRunCommandAndAccessibleModal() + { + HelpDialogTestHelpers.AllowDialogOpen(this); + IRenderedComponent cut = Render(); + + Assert.Equal(RunCommandDialog.CommandText, cut.Find(".run-command-text").TextContent); + Assert.Equal("dialog", cut.Find("dialog").GetAttribute("role")); + Assert.Equal("true", cut.Find("dialog").GetAttribute("aria-modal")); + Assert.Equal("Copy", cut.Find(".dialog-actions button").TextContent.Trim()); + } + + [Fact] + public void Dialog_CopyReportsSuccess() + { + HelpDialogTestHelpers.AllowDialogOpen(this); + IRenderedComponent cut = Render(); + + cut.Find(".dialog-actions button").Click(); + + Assert.Contains("Command copied", cut.Markup); + } + + [Fact] + public void Dialog_CopyFailureKeepsCommandVisibleAndReportsFailure() + { + HelpDialogTestHelpers.AllowDialogOpen(this); + HelpDialogTestHelpers.FailClipboardCopy(this); + IRenderedComponent cut = Render(); + + cut.Find(".dialog-actions button").Click(); + + Assert.Contains(RunCommandDialog.CommandText, cut.Markup); + Assert.Contains("Copying was not available", cut.Markup); + } +} diff --git a/BlogWriter.Web.Tests/WorkflowLogTests.cs b/BlogWriter.Web.Tests/WorkflowLogTests.cs index 7a9502f..9dda414 100644 --- a/BlogWriter.Web.Tests/WorkflowLogTests.cs +++ b/BlogWriter.Web.Tests/WorkflowLogTests.cs @@ -29,4 +29,18 @@ public void WorkflowLog_RendersOrderedTextEntries() Assert.Equal(["Progress: started", "Failure: "], cut.FindAll("li").Select(item => item.TextContent).ToArray()); Assert.Contains("<failed>", cut.Markup); } + + [Fact] + public void WorkflowLog_RetainsOlderEntriesForScrolling() + { + IReadOnlyList entries = Enumerable.Range(1, 5) + .Select(index => new WorkflowLogEntry($"entry {index}", WorkflowOutputOutcome.Progress)) + .ToList(); + + IRenderedComponent cut = Render(parameters => parameters + .Add(component => component.Entries, entries)); + + Assert.Equal(5, cut.FindAll("li").Count); + Assert.Contains("workflow-log", cut.Find("section").ClassList); + } } \ No newline at end of file diff --git a/BlogWriter.Web.Tests/WorkspaceBrowserTests.cs b/BlogWriter.Web.Tests/WorkspaceBrowserTests.cs index 87052f5..c257f67 100644 --- a/BlogWriter.Web.Tests/WorkspaceBrowserTests.cs +++ b/BlogWriter.Web.Tests/WorkspaceBrowserTests.cs @@ -1,6 +1,11 @@ +using Bunit; +using BlogWriter.Web.Components.Pages; +using BlogWriter.Web.Services; +using Microsoft.Extensions.DependencyInjection; + namespace BlogWriter.Web.Tests; -public sealed class WorkspaceBrowserTests +public sealed class WorkspaceBrowserTests : BunitContext { [Theory] [InlineData(390, 844)] @@ -10,4 +15,73 @@ public void RequiredViewports_AreExplicitlyCovered(int width, int height) Assert.Contains((width, height), new[] { (390, 844), (1440, 900) }); } + [Fact] + public void OutputLayoutContract_CapsLogAndProtectsCompactSelector() + { + string css = FindRepositoryFile("BlogWriter.Web", "wwwroot", "app.css"); + + Assert.Contains(".list-command input", css); + Assert.Contains("width: 3.5rem", css); + Assert.Contains("max-height: 4.8rem", css); + Assert.Contains("overflow: auto", css); + Assert.Contains("grid-template-columns: repeat(5, minmax(0, 1fr))", css); + } + + [Fact] + public void ListSelectionFixture_LaunchesSavedSessionWithRestoredPromptContext() + { + var sessions = new BrowserSessionService(); + Services.AddSingleton(new BlogWorkspaceService(sessions, TimeSpan.FromMilliseconds(25))); + IRenderedComponent cut = Render(); + + cut.Find("button[data-command='list']").Click(); + cut.WaitForElement("#command-session-number").Change("1"); + + cut.WaitForAssertion(() => + { + Assert.Equal(1, sessions.StartCalls); + Assert.Equal("saved topic", sessions.LastPrompt); + Assert.Equal("tighten the ending", cut.Find("#revision-prompt").GetAttribute("value")); + }); + } + + private static string FindRepositoryFile(params string[] segments) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + string path = Path.Combine([directory.FullName, .. segments]); + if (File.Exists(path)) + { + return File.ReadAllText(path); + } + + directory = directory.Parent; + } + + throw new FileNotFoundException("Could not locate repository file.", Path.Combine(segments)); + } + + private sealed class BrowserSessionService : IBlogWriterSessionService + { + public int StartCalls { get; private set; } + public string? LastPrompt { get; private set; } + + public Task StartAsync(string prompt, int minWords = ResearchState.DefaultMinWords, int maxWords = ResearchState.DefaultMaxWords, CancellationToken cancellationToken = default, IProgress? output = null) + { + StartCalls++; + LastPrompt = prompt; + return Task.FromResult(ListLauncherTestHelpers.Session(prompt, "tighten the ending", "new draft", "new review")); + } + + public Task ReviseAsync(BlogSession session, string revision, int minWords, int maxWords, CancellationToken cancellationToken = default, IProgress? output = null) => + Task.FromResult(session); + + public Task> ListAsync(CancellationToken cancellationToken = default) => + Task.FromResult>([ListLauncherTestHelpers.Summary("saved topic")]); + + public Task LoadAsync(string sessionId, CancellationToken cancellationToken = default) => + Task.FromResult(ListLauncherTestHelpers.Session("saved topic", "tighten the ending")); + } + } \ No newline at end of file diff --git a/BlogWriter.Web/Components/CommandBar.razor b/BlogWriter.Web/Components/CommandBar.razor index 1d8e5f3..a74527d 100644 --- a/BlogWriter.Web/Components/CommandBar.razor +++ b/BlogWriter.Web/Components/CommandBar.razor @@ -1,8 +1,28 @@ @code { @@ -12,12 +32,30 @@ [Parameter] public EventCallback List { get; set; } + [Parameter] + public EventCallback SelectionChanged { get; set; } + + [Parameter] + public bool SelectionVisible { get; set; } + + [Parameter] + public string SelectionInput { get; set; } = ""; + + [Parameter] + public string? SelectionError { get; set; } + [Parameter] public EventCallback Revise { get; set; } [Parameter] public EventCallback Quit { get; set; } + [Parameter] + public EventCallback Help { get; set; } + [Parameter] public bool ReviseEnabled { get; set; } + + private Task HandleSelectionChanged(ChangeEventArgs args) => + SelectionChanged.InvokeAsync(args.Value?.ToString() ?? ""); } diff --git a/BlogWriter.Web/Components/Pages/Home.razor b/BlogWriter.Web/Components/Pages/Home.razor index a17609e..510c2d2 100644 --- a/BlogWriter.Web/Components/Pages/Home.razor +++ b/BlogWriter.Web/Components/Pages/Home.razor @@ -38,7 +38,7 @@ Value="@Workspace.State.RevisionPrompt" ValueChanged="value => Workspace.State.RevisionPrompt = value" Submit="Workspace.SubmitRevisionAsync" - Disabled="@(Workspace.State.IsProcessing || Workspace.State.ActiveSession is null)" /> + Disabled="@(!Workspace.State.IsReviseEnabled)" /> - @if (Workspace.State.IsSelectionVisible) - { -
- - -
- } } @@ -98,10 +93,16 @@ } +@if (_showHelpDialog) +{ + +} + @code { private PromptInput? _initialPrompt; private PendingAction _pendingAction; private bool _showDiscardDialog; + private bool _showHelpDialog; protected override void OnInitialized() => Workspace.Changed += HandleWorkspaceChanged; @@ -139,6 +140,10 @@ _pendingAction = PendingAction.None; } + private void ShowHelp() => _showHelpDialog = true; + + private void CloseHelp() => _showHelpDialog = false; + private Task ExecuteTransitionAsync(PendingAction action, bool discardConfirmed) => action switch { diff --git a/BlogWriter.Web/Components/RunCommandDialog.razor b/BlogWriter.Web/Components/RunCommandDialog.razor new file mode 100644 index 0000000..30aabb0 --- /dev/null +++ b/BlogWriter.Web/Components/RunCommandDialog.razor @@ -0,0 +1,69 @@ +@inject IJSRuntime JS +@implements IAsyncDisposable + +
+ +

Developer help

+

Run Blog Writer

+

Use this command in the repository terminal:

+ @CommandText + @if (!string.IsNullOrWhiteSpace(CopyMessage)) + { +

@CopyMessage

+ } +
+ + +
+
+
+ +@code { + public const string CommandText = "dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https"; + + private ElementReference _dialog; + private bool _opened; + + [Parameter] + public EventCallback Closed { get; set; } + + private string? CopyMessage { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JS.InvokeVoidAsync("blogWriterDialog.show", _dialog); + _opened = true; + } + } + + private async Task CopyAsync() + { + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", CommandText); + CopyMessage = "Command copied."; + } + catch (JSException) + { + CopyMessage = "Copying was not available. Select the command above to copy it manually."; + } + } + + private async Task Close() => await Closed.InvokeAsync(); + + public async ValueTask DisposeAsync() + { + if (_opened) + { + try + { + await JS.InvokeVoidAsync("blogWriterDialog.close", _dialog); + } + catch (JSDisconnectedException) + { + } + } + } +} \ No newline at end of file diff --git a/BlogWriter.Web/Services/BlogWorkspaceService.cs b/BlogWriter.Web/Services/BlogWorkspaceService.cs index 4b8561b..83faf06 100644 --- a/BlogWriter.Web/Services/BlogWorkspaceService.cs +++ b/BlogWriter.Web/Services/BlogWorkspaceService.cs @@ -107,6 +107,7 @@ public async Task ListAsync(bool discardConfirmed) State.InitialPrompt = ""; State.RevisionPrompt = ""; State.SelectionInput = ""; + State.SelectionError = null; State.ValidationMessage = null; State.StatusMessage = "Loading saved sessions..."; State.AppendLog(State.StatusMessage, WorkflowOutputOutcome.Progress); @@ -159,6 +160,53 @@ public async Task LoadSelectionAsync() } } + public async Task LaunchSelectionAsync(string input) + { + State.SelectionInput = input; + State.SelectionError = null; + + if (State.IsProcessing) + { + SetSelectionError("Wait for the current writing operation to finish."); + return; + } + + if (!SessionListSelection.TryResolve(input, State.DisplayedSessions, out BlogSessionSummary? summary)) + { + SetSelectionError("Enter a valid number from the current saved-session list."); + return; + } + + BlogSession? session; + try + { + session = await _sessions.LoadAsync(summary!.Id); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + SetSelectionError("Unable to load the selected session. Try again."); + return; + } + + if (session is null) + { + SetSelectionError("That saved session is no longer available. Refresh the list and try again."); + return; + } + + State.Draft = ""; + State.Review = ""; + State.ReviewerUpdateKeys.Clear(); + State.InitialPrompt = session.State.MainTask; + State.RevisionPrompt = session.State.CurrentSubTask; + State.ActiveSession = null; + State.SelectionError = null; + State.ValidationMessage = null; + NotifyChanged(); + + await SubmitInitialAsync(); + } + public async Task QuitAsync(bool discardConfirmed) { if (RequiresDiscardConfirmation(discardConfirmed)) @@ -354,6 +402,7 @@ private void Publish(BlogSession session, WordRange? submittedRange = null) } State.DisplayedSessions = []; State.SelectionInput = ""; + State.SelectionError = null; State.Mode = WorkspaceMode.Draft; State.ValidationMessage = null; NotifyChanged(); @@ -371,6 +420,7 @@ private void ClearWorkspace(WorkspaceMode mode) State.ActiveSession = null; State.DisplayedSessions = []; State.SelectionInput = ""; + State.SelectionError = null; State.IsProcessing = false; State.StatusMessage = null; State.ValidationMessage = null; @@ -384,6 +434,14 @@ private void SetValidation(string message) NotifyChanged(); } + private void SetSelectionError(string message) + { + State.SelectionError = message; + State.ValidationMessage = message; + State.AppendLog(message, WorkflowOutputOutcome.Validation); + NotifyChanged(); + } + private void HandleOutput(long version, WorkflowOutputUpdate update) { if (version != _operationVersion || State.Mode == WorkspaceMode.Ended) diff --git a/BlogWriter.Web/Services/BlogWorkspaceState.cs b/BlogWriter.Web/Services/BlogWorkspaceState.cs index 593cd22..212652a 100644 --- a/BlogWriter.Web/Services/BlogWorkspaceState.cs +++ b/BlogWriter.Web/Services/BlogWorkspaceState.cs @@ -31,6 +31,7 @@ public sealed class BlogWorkspaceState public BlogSession? ActiveSession { get; internal set; } public IReadOnlyList DisplayedSessions { get; internal set; } = []; public string SelectionInput { get; set; } = ""; + public string? SelectionError { get; internal set; } public bool IsProcessing { get; internal set; } public string? StatusMessage { get; internal set; } public string? ValidationMessage { get; internal set; } @@ -72,7 +73,7 @@ internal void AppendReviewerFeedback(string message, string updateKey) : $"{Review}\n\n{message}"; } - public bool IsSelectionVisible => Mode == WorkspaceMode.List && DisplayedSessions.Count > 0; + public bool IsSelectionVisible => Mode == WorkspaceMode.List; public bool IsReviseEnabled => IsSelectionVisible && !IsProcessing; public bool HasUnsavedRange { diff --git a/BlogWriter.Web/wwwroot/app.css b/BlogWriter.Web/wwwroot/app.css index 8ae7bcb..713c2bf 100644 --- a/BlogWriter.Web/wwwroot/app.css +++ b/BlogWriter.Web/wwwroot/app.css @@ -136,7 +136,11 @@ button, a { touch-action: manipulation; } .session-entry time { margin-top: 5px; color: var(--muted); font-size: .7rem; } .workspace-footer { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 12px 16px; } -.command-bar { display: grid; grid-template-columns: repeat(4, 82px); gap: 8px; } +.command-bar { display: grid; grid-template-columns: 82px minmax(0, 1fr) 82px 82px 36px; gap: 8px; } +.list-command { min-width: 0; display: flex; align-items: stretch; gap: 6px; } +.list-command button { flex: 0 0 82px; } +.list-command input { width: 3.5rem; min-width: 3.5rem; border: 1px solid var(--ink); padding: 6px 5px; background: var(--surface); color: var(--ink); text-align: center; } +.help-command { padding-inline: 0 !important; } .command-bar button, .dialog-actions button, .command-button { min-height: 36px; border: 1px solid var(--ink); @@ -153,7 +157,7 @@ button, a { touch-action: manipulation; } .session-selector { width: 150px; grid-column: 2; grid-row: 1 / span 2; display: grid; align-self: end; gap: 5px; } .session-selector input { width: 100%; min-height: 36px; border: 1px solid var(--ink); padding: 6px 9px; background: white; } -.workflow-log { grid-column: 1; min-width: 0; max-height: 8rem; overflow: auto; border-top: 1px solid var(--line); padding: 8px 0 0; } +.workflow-log { grid-column: 1; min-width: 0; max-height: 4.8rem; overflow: auto; border-top: 1px solid var(--line); padding: 8px 0 0; } .workflow-log-list { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; } .workflow-log-entry { margin: 0; font-size: .78rem; line-height: 1.35; overflow-wrap: anywhere; } .workflow-log-progress { color: var(--muted); } @@ -183,8 +187,11 @@ button, a { touch-action: manipulation; } .work-grid { min-height: 0; } .pane-content { max-height: 38vh; } .workspace-footer { display: flex; align-items: stretch; flex-direction: column; } - .command-bar { width: 100%; grid-template-columns: repeat(4, minmax(0, 1fr)); } - .workflow-log { max-height: 10rem; } + .command-bar { width: 100%; grid-template-columns: repeat(5, minmax(0, 1fr)); } + .list-command { gap: 4px; } + .list-command button { flex-basis: auto; min-width: 0; } + .list-command input { width: 3rem; min-width: 3rem; padding-inline: 2px; } + .workflow-log { max-height: 4.8rem; } .session-selector { width: 100%; } } diff --git a/README.md b/README.md index 4baf805..39d6857 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ they default to 1000 and 2000 words. Workflow progress, validation, cancellation failure messages appear as an ordered log beneath the New/List/Revise/Quit buttons. Reviewer feedback is kept in Reviewer notes as it arrives and accumulates across revisions for the active session; it is cleared when starting New or loading another -session. +session. In List mode, enter the one-based session number beside List to restore the +saved MainTask and optional CurrentSubTask and launch it immediately. The `?` command +shows and copies the HTTPS launch command. After configuring Microsoft Entra, Foundry, and Cosmos values from [docs/configuration.md](docs/configuration.md), start it with: diff --git a/docs/configuration.md b/docs/configuration.md index 47d2a32..55909e3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -100,6 +100,11 @@ Reviewer notes pane as it becomes available and is retained across revisions for active session. New and loading a different saved session clear transient log history and reviewer history; incremental output is not persisted as a separate record. +In List mode, the inline three-digit selector uses the displayed one-based position. +Valid selection restores `MainTask`, restores non-empty `CurrentSubTask` as the Revision +request, and starts one initial writing operation. The `?` command displays and copies +the HTTPS launch command. Revision request is disabled whenever Revise is disabled. + ## Keeping prompts in sync Each hosted agent's `AgentPrompt.cs` must be kept in sync with the corresponding section diff --git a/docs/~configuration.md.saved.bak b/docs/~configuration.md.saved.bak new file mode 100644 index 0000000..55909e3 --- /dev/null +++ b/docs/~configuration.md.saved.bak @@ -0,0 +1,112 @@ +# Configuration reference + +All values below are read from environment variables, with `dotnet user-secrets` +recommended for local development of the console app (secrets win over environment +variables on key collisions). None of the four hosted agents or the console app use API +keys — every credential is Microsoft Entra ID (`AzureCliCredential` locally, +`DefaultAzureCredential` in hosted agents). + +## Console app (`BlogWriter.csproj`, root `Program.cs`) + +| Key | Required | Default | Notes | +| --- | --- | --- | --- | +| `FOUNDRY_PROJECT_ENDPOINT` | yes | — | e.g. `https://.services.ai.azure.com/api/projects/` | +| `AZURE_TENANT_ID` | yes | — | Microsoft Entra tenant hosting the Foundry project | +| `BLOGGER_AGENT_NAME` | no | `Blogger` | Name of the deployed hosted agent to call | +| `RESEARCHER_AGENT_NAME` | no | `Researcher` | | +| `AUTHOR_AGENT_NAME` | no | `Author` | | +| `REVIEWER_AGENT_NAME` | no | `Reviewer` | | +| `MAX_TOTAL_TOKENS` | no | `40000` | Cumulative cross-agent token cap (`TokenCapChatClient`); parse failures fall back to the default | +| `COSMOS_ENDPOINT` | yes | — | URI of the Azure Cosmos DB for NoSQL account; authenticates with Microsoft Entra ID | +| `COSMOS_DATABASE_NAME` | yes | — | Database containing BlogWriter session documents | +| `COSMOS_CONTAINER_NAME` | yes | — | Owner-partitioned container containing BlogWriter session documents | + +Set with, e.g.: + +```powershell +dotnet user-secrets set "FOUNDRY_PROJECT_ENDPOINT" "https://.services.ai.azure.com/api/projects/" +dotnet user-secrets set "AZURE_TENANT_ID" "" +``` + +## Each hosted agent (`HostedAgents/Blogger`, `Researcher`, `Author`, `Reviewer`) + +| Key | Required | Default | Notes | +| --- | --- | --- | --- | +| `FOUNDRY_PROJECT_ENDPOINT` | yes | — | Same Foundry project the console app points at | +| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | no | `gpt-5-mini` | Model deployment used by that specific agent; set per-project in its own `azure.yaml` | + +These are set as `environmentVariables` in each project's `azure.yaml` and provisioned by +`azd` — see [deployment.md](deployment.md). They're not read from `dotnet user-secrets` +since hosted agents run in Azure, not locally, once deployed. + +## Blazor web app (`BlogWriter.Web/BlogWriter.Web.csproj`) + +The web host uses Microsoft Entra OpenID Connect for user sign-in. The signed-in +user's `oid` claim owns session data; a separate Azure credential authorizes the +server to call Foundry and Cosmos. + +The web host uses the authorization-code flow with PKCE. In the Entra app +registration, configure `https://localhost:7056/signin-oidc` as a **Web** redirect +URI. Do not enable the implicit-grant access-token or ID-token checkboxes. + +| Key | Required | Default | Notes | +| --- | --- | --- | --- | +| `AzureAd:TenantId` | yes | — | Entra tenant for user sign-in | +| `AzureAd:ClientId` | yes | — | Web app registration client ID | +| `AzureAd:ClientSecret` | local only | — | Store in user secrets; never commit | +| `AzureAd:CallbackPath` | no | `/signin-oidc` | Must match the app registration redirect URI | +| `AzureAd:ClientCredentials:0:SourceType` | production | `KeyVault` | Certificate credential source | +| `AzureAd:ClientCredentials:0:KeyVaultUrl` | production | — | Key Vault containing the OIDC certificate | +| `AzureAd:ClientCredentials:0:KeyVaultCertificateName` | production | — | Certificate name registered with the Entra app | +| `AzureResources:CredentialMode` | yes | `AzureCli` | `AzureCli` locally; `ManagedIdentity` in production | +| `Foundry:ProjectEndpoint` | yes | — | Existing Foundry project endpoint | +| `Foundry:*AgentName` | no | role name | Existing hosted-agent names | +| `Foundry:MaxTotalTokens` | no | `40000` | Shared process token cap | +| `Cosmos:Endpoint` | yes | — | Cosmos account endpoint | +| `Cosmos:DatabaseName` | yes | — | Session database | +| `Cosmos:ContainerName` | yes | — | Owner-partitioned session container | + +Local setup: + +```powershell +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "AzureAd:TenantId" "" +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "AzureAd:ClientId" "" +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "AzureAd:ClientSecret" "" +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "Foundry:ProjectEndpoint" "https://.services.ai.azure.com/api/projects/" +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "Cosmos:Endpoint" "https://.documents.azure.com:443/" +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "Cosmos:DatabaseName" "blogwriter" +dotnet user-secrets --project BlogWriter.Web/BlogWriter.Web.csproj set "Cosmos:ContainerName" "sessions" +``` + +`Authentication:UseTestingIdentity` is accepted only when the host environment is +`Testing`. It exists for automated browser checks and must never be enabled in a +development, staging, or production deployment. + +### Word-count controls + +The web workspace displays `Min` and `Max` between the prompt inputs and the +Draft/Reviewer panes. They default to 1000 and 2000, accept positive whole numbers, +and require Max to be at least Min. The values are session state, not configuration +keys: loading a saved session restores its range, and a successful revision persists +the updated range. Unsaved range changes participate in the existing New/List/Quit +discard confirmation. + +### Workflow log and Reviewer notes + +The web workspace places an ordered workflow log directly beneath the command buttons. +It contains progress, success, validation, cancellation, conflict, and failure output; +the separate status/validation stack is not used. Reviewer feedback is routed to the +Reviewer notes pane as it becomes available and is retained across revisions for the +active session. New and loading a different saved session clear transient log history +and reviewer history; incremental output is not persisted as a separate record. + +In List mode, the inline three-digit selector uses the displayed one-based position. +Valid selection restores `MainTask`, restores non-empty `CurrentSubTask` as the Revision +request, and starts one initial writing operation. The `?` command displays and copies +the HTTPS launch command. Revision request is disabled whenever Revise is disabled. + +## Keeping prompts in sync + +Each hosted agent's `AgentPrompt.cs` must be kept in sync with the corresponding section +of the console app's `Prompts.cs`. There's no automated check for this today — when +changing one, update the other. diff --git a/specs/006-list-launcher-workflow/checklists/requirements.md b/specs/006-list-launcher-workflow/checklists/requirements.md new file mode 100644 index 0000000..15483bd --- /dev/null +++ b/specs/006-list-launcher-workflow/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: List Launcher Workflow + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-09-20 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- The run command is documented as a user-visible default assumption so the Help dialog has a deterministic acceptance target. +- The three-line log requirement is scoped to the visible viewport; retained history remains available by scrolling. +- Existing saved-session ownership, workflow, word-count, cancellation, and accessibility contracts remain dependencies rather than new scope. diff --git a/specs/006-list-launcher-workflow/contracts/list-launcher-ui.md b/specs/006-list-launcher-workflow/contracts/list-launcher-ui.md new file mode 100644 index 0000000..7255ff9 --- /dev/null +++ b/specs/006-list-launcher-workflow/contracts/list-launcher-ui.md @@ -0,0 +1,44 @@ +# List Launcher UI Contract + +This is an in-process UI/state contract. No public HTTP endpoint or persisted document shape is added. + +## Command Bar + +- Existing commands remain New, List, Revise, and Quit. +- A compact numeric selector appears immediately beside List while List mode is active. +- Commands to the right of List shift right and remain operable. +- A compact `?` Help button appears beside the existing commands. +- The former bottom-of-form session-number selector is not rendered. + +## Selection + +- The displayed list is one-based and capped by the existing list behavior. +- Whitespace around the selector is accepted; empty, non-integer, non-positive, and out-of-range values are rejected. +- Invalid selection appends a validation entry to the workflow log and performs no load or workflow call. +- A valid selection is ignored/rejected while processing is active. +- A valid selection clears Draft, Reviewer notes, New writing prompt, and Revision request before restoration. +- `MainTask` becomes New writing prompt. +- Non-empty `CurrentSubTask` becomes Revision request; empty `CurrentSubTask` leaves it empty. +- After restoration, exactly one existing initial-writing operation begins from `MainTask`. + +## Revision Input + +- Revision request is disabled whenever Revise is disabled. +- Revision request is enabled only when an active/list-selected session is eligible and no operation is processing. +- Existing Enter/Shift+Enter prompt behavior remains unchanged. + +## Help Dialog + +- The dialog has an accessible name and modal semantics. +- It displays exactly: `dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https`. +- Copy action attempts clipboard copy of the displayed command. +- Success is communicated without hiding the command. +- Failure leaves the command visible and communicates that copying did not complete. +- Closing the dialog leaves workspace state unchanged and restores usable focus. + +## Workflow Log + +- Retained entries remain in chronological order. +- The visible scroll viewport displays three normal log-entry lines. +- Older entries remain reachable by scrolling. +- Log content remains text-safe and the viewport remains usable at 390 × 844 and 1440 × 900. diff --git a/specs/006-list-launcher-workflow/data-model.md b/specs/006-list-launcher-workflow/data-model.md new file mode 100644 index 0000000..3a1dc1c --- /dev/null +++ b/specs/006-list-launcher-workflow/data-model.md @@ -0,0 +1,62 @@ +# Data Model: List Launcher Workflow + +No persisted schema change is required. The feature reuses existing saved `ResearchState` fields and adds transient circuit-scoped UI state. + +## List Selection Input + +| Field | Type | Rules | +| --- | --- | --- | +| Value | String | Preserves current text, including invalid input; surrounding whitespace is trimmed for validation | +| DisplayPosition | Positive integer | One-based position in the current displayed saved-session list | +| Error | String or null | Set for empty, non-numeric, non-positive, out-of-range, or processing-conflict input | +| IsVisible | Boolean | True while the workspace is in List mode with the command-bar selector active | + +## Restored Session Prompts + +| Field | Source | Rules | +| --- | --- | --- | +| NewPrompt | `ResearchState.MainTask` | Required for automatic initial processing; rendered as text | +| RevisionPrompt | `ResearchState.CurrentSubTask` | Non-empty value is restored; empty value remains empty | +| SessionId | Existing `BlogSession.Id` | Internal identity used for owner-scoped load; not entered by the user | + +## Run Command Help Dialog + +| Field | Type | Rules | +| --- | --- | --- | +| CommandText | String | `dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https` | +| IsOpen | Boolean | Transient dialog state | +| CopyStatus | Idle, Copied, Failed | Visible feedback after clipboard action; command remains visible on failure | + +## Workflow Log Viewport + +| Field | Type | Rules | +| --- | --- | --- | +| Entries | Existing ordered collection | Retains all accepted log entries | +| VisibleLineLimit | Integer | Fixed at 3 visible normal entry lines | +| Scrollable | Boolean | True when retained entries exceed the visible viewport | + +## Workspace State Changes + +| From | Event | Result | +| --- | --- | --- | +| Any non-list mode | Click List and transition accepted | Enter List mode, load displayed summaries, show compact selector beside List | +| List mode | Invalid selector input | Keep list and selector visible; append validation outcome; perform no load/workflow call | +| List mode | Valid selector input | Clear Draft, Reviewer notes, both prompt fields, and transient selection state before loading selected session | +| Valid selector input | Session loaded | Restore `MainTask` and optional `CurrentSubTask`; start exactly one initial workflow operation using `MainTask` | +| Valid selector input | Load/process failure | Preserve existing error/log behavior; do not discard unrelated current state beyond the required selection reset | +| Any state | Revise unavailable | Disable Revise and Revision request together | +| Any state | Help opened | Show command dialog without changing workspace content | +| Help dialog | Copy succeeds | Set CopyStatus to Copied; retain command | +| Help dialog | Copy fails | Set CopyStatus to Failed; retain command and dialog | +| Any state | New/List/Quit transitions | Preserve existing discard confirmation and cancellation behavior | + +## Validation Rules + +- Trim selector text before parsing. +- Require a positive whole number. +- Require the number to be within `1..DisplayedSessions.Count`. +- Reject selector changes while another workflow operation is processing. +- Do not call `LoadAsync` or start processing for invalid input. +- Automatic launch uses the restored `MainTask`; restored `CurrentSubTask` is UI context and remains available as Revision request. +- Clipboard failure is non-fatal and must not change session/workspace content. +- Log history remains retained while only the visible viewport is capped at three lines. diff --git a/specs/006-list-launcher-workflow/plan.md b/specs/006-list-launcher-workflow/plan.md new file mode 100644 index 0000000..73e8300 --- /dev/null +++ b/specs/006-list-launcher-workflow/plan.md @@ -0,0 +1,125 @@ +# Implementation Plan: List Launcher Workflow + +**Branch**: `006-list-launcher-workflow` | **Date**: 2026-09-20 | **Spec**: [spec.md](spec.md) + +**Input**: Feature specification from `/specs/006-list-launcher-workflow/spec.md` + +**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow. + +## Summary + +Move saved-session selection into the command bar, replace the bottom selector, and +launch a selected session's `MainTask` through the existing initial-writing workflow +after restoring `CurrentSubTask` when present. Add a small Help action that displays and +copies the HTTPS launch command, disable Revision request whenever Revise is unavailable, +and constrain the workflow-log viewport to three visible lines while retaining history. + +The design keeps session identity and persistence in the existing session service, keeps +selection validation in the circuit-scoped workspace service, and uses focused Razor +components for the command-bar input and Help dialog. It does not add a database field, +new workflow edge, hosted agent, model call, or authentication path. + +## Technical Context + +**Language/Version**: C# / .NET 10, Razor components, HTML, and CSS + +**Primary Dependencies**: Existing Interactive Server Blazor host, `BlogWorkspaceService`, +`BlogWorkspaceState`, `IBlogWriterSessionService`, `ResearchState`, existing dialog and +JavaScript interop patterns, xUnit, bUnit, and Playwright/browser checks; no new runtime +package + +**Storage**: No schema change. Saved `ResearchState.MainTask` and +`ResearchState.CurrentSubTask` are reused; list selection remains owner-scoped through +the existing session service. + +**Testing**: Focused xUnit and bUnit workspace/component tests, full core/web regression, +responsive browser checks at 390 × 844 and 1440 × 900, and clipboard/dialog behavior tests + +**Target Platform**: Existing server-hosted responsive Blazor application + +**Project Type**: Existing shared application assembly plus Interactive Server Blazor web +host and web test project + +**Performance Goals**: Valid selection starts exactly one existing workflow operation; +invalid input performs no load or workflow request; Help opens without a server request; +log rendering remains bounded to three visible lines + +**Constraints**: One-based displayed-list numbering; three-digit compact input; no bottom +selector; commands to the right remain operable; Help copies +`dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https`; +`CurrentSubTask` is the optional restored Revision request; preserve discard confirmation, +session ownership, cancellation, word-count controls, workflow termination, safe text +rendering, and WCAG 2.2 AA + +**Scale/Scope**: One command-bar input, one Help dialog, and one bounded log viewport in +the existing workspace; at most the existing displayed session list; no new persistence, +API, hosted agent, or deployment resource + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +**Pre-design gate: PASS. Post-design gate: PASS.** + +| Principle | Plan response | Status | +| --- | --- | --- | +| Hosted-Agent Boundaries | Selection uses the existing session service and initial workflow; no hosted-agent or raw HTTP changes. | Pass | +| MAF-Native Workflow Composition | Automatic launch reuses the existing initial workflow and does not add edges or alter bounded revision routing. | Pass | +| Identity, Secrets, and Budget Control | No credential, secret, or token-budget changes; Help exposes a local run command only. | Pass | +| Testable and Observable Behavior | Selection, restoration, disabled states, dialog copy, and log viewport receive focused deterministic tests and browser checks. | Pass | +| Simple, Compatible Evolution | Reuses `MainTask`, `CurrentSubTask`, existing session ownership, and existing workspace transitions; no schema migration. | Pass | + +MAF Doctor baseline is **F** with 4 existing errors, 3 warnings, 0 silent-starvation +risks, and 6 heuristic uncapped-call matches. This feature must not add findings or +change workflow topology. + +## Project Structure + +### Documentation (this feature) + +```text +specs/006-list-launcher-workflow/ +├── plan.md # This file (/speckit-plan command output) +├── research.md # Phase 0 output (/speckit-plan command) +├── data-model.md # Phase 1 output (/speckit-plan command) +├── quickstart.md # Phase 1 output (/speckit-plan command) +├── contracts/ # Phase 1 output (/speckit-plan command) +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) +``` + +### Source Code (repository root) + +```text +./ +├── BlogWriter.Web/ +│ ├── Components/CommandBar.razor # List input and Help action +│ ├── Components/RunCommandDialog.razor # Accessible command/copy dialog +│ ├── Components/Pages/Home.razor # Remove bottom selector and wire controls +│ ├── Components/PromptInput.razor # Preserve disabled Revision request state +│ ├── Services/BlogWorkspaceState.cs # Selection input and restored prompts +│ ├── Services/BlogWorkspaceService.cs # Validate, load, restore, and auto-start +│ └── wwwroot/app.css # Command layout and three-line log viewport +├── BlogWriter.Tests/ +│ └── SessionListSelectionTests.cs # Selection validation/numbering tests +├── BlogWriter.Web.Tests/ +│ ├── BlogWorkspaceServiceTests.cs # Restore and automatic processing tests +│ ├── CommandBarTests.cs # Layout/input/disabled-state tests +│ ├── RunCommandDialogTests.cs # Dialog and copy behavior tests +│ ├── HomePageTests.cs # Placement and removal tests +│ └── WorkspaceBrowserTests.cs # Responsive/log viewport checks +└── README.md / docs/configuration.md # User-facing behavior documentation +``` + +**Structure Decision**: Keep selection and automatic-launch orchestration in the existing +circuit-scoped workspace service. Keep command-bar presentation and Help/copy behavior in +focused Razor components, while reusing the existing discard dialog and JavaScript +interop conventions. Continue using `BlogSession.State.MainTask` and +`BlogSession.State.CurrentSubTask`; do not introduce a new persistence model. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| None | N/A | The feature fits the existing workspace, session-service, component, and test boundaries. | diff --git a/specs/006-list-launcher-workflow/quickstart.md b/specs/006-list-launcher-workflow/quickstart.md new file mode 100644 index 0000000..bcfa732 --- /dev/null +++ b/specs/006-list-launcher-workflow/quickstart.md @@ -0,0 +1,72 @@ +# Quickstart: List Launcher Workflow + +## Prerequisites + +- Existing BlogWriter solution and web test project +- .NET 10 SDK +- No live Foundry, Cosmos, or Entra dependency for focused tests +- Read [list-launcher-ui.md](contracts/list-launcher-ui.md) for the interaction contract + +## Focused automated validation + +From the repository root: + +```powershell +dotnet test BlogWriter.Tests/BlogWriter.Tests.csproj --filter "FullyQualifiedName~SessionListSelectionTests" +dotnet test BlogWriter.Web.Tests/BlogWriter.Web.Tests.csproj --filter "FullyQualifiedName~BlogWorkspaceServiceTests|FullyQualifiedName~CommandBarTests|FullyQualifiedName~RunCommandDialogTests|FullyQualifiedName~HomePageTests" +``` + +Expected outcome: valid selection launches exactly one operation, invalid selection +starts none, MainTask/CurrentSubTask restoration is correct, the bottom selector is +absent, Help copy behavior is covered, Revision request follows Revise availability, +and the log viewport is bounded to three visible lines. + +Automated checkpoint: 88 core tests and 58 web tests pass; both projects build cleanly. + +Browser checkpoint: Help, exact command text, disabled Revision request, and responsive +containment passed at 390 × 844 and 1440 × 900. The Testing environment's placeholder +Cosmos endpoint did not return saved sessions, so inline selection was validated through +the focused workspace/service tests rather than a live browser session list. + +## Manual workspace validation + +1. Start the web application with `dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https` and sign in. +2. Click List and confirm a compact numeric input appears beside List; confirm the selector is not present at the bottom of the form. +3. Confirm New, List, Revise, Quit, and `?` remain visible and the commands to the right of List remain operable. +4. Enter an invalid selector such as empty text, `0`, `-1`, `1.5`, text, or a number outside the displayed list; confirm no session loads or workflow starts and a log validation entry appears. +5. Enter a valid displayed number; confirm Draft, Reviewer notes, New writing prompt, and Revision request clear before the selected prompt data is restored. +6. Confirm the selected session's MainTask fills New writing prompt and a non-empty CurrentSubTask fills Revision request. +7. Confirm processing starts exactly once from the restored New writing prompt. +8. Open Help, verify the HTTPS run command, copy it, and confirm success feedback. Test clipboard denial/failure if available and confirm the command remains visible. +9. Disable Revise by leaving no eligible selected session and confirm Revision request is disabled; make Revise eligible and confirm it becomes enabled when not processing. +10. Produce more than three log entries and confirm the visible log viewport holds three lines while older entries remain scrollable. + +## Responsive and accessibility validation + +At 390 × 844 and 1440 × 900: + +- List selector is beside List, is wide enough for three digits, and does not overlap commands. +- The Help button has an accessible name and the dialog has modal labeling and keyboard dismissal. +- Revision request disabled state matches Revise disabled state. +- Workflow log shows three lines without horizontal overflow and retains scrollability. +- Prompt and restored session values are readable and text-safe. +- Existing labels, focus order, and WCAG 2.2 Level A/AA behavior remain intact. + +## Full regression + +```powershell +dotnet test BlogWriter.Tests/BlogWriter.Tests.csproj +dotnet test BlogWriter.Web.Tests/BlogWriter.Web.Tests.csproj +dotnet build BlogWriter.csproj +dotnet build BlogWriter.Web/BlogWriter.Web.csproj +git diff --check +``` + +Existing authentication, session ownership, Cosmos persistence, cancellation, word-count +controls, workflow termination, Reviewer notes routing, and MAF topology must remain unchanged. + +## MAF Doctor comparison + +Run MAF Doctor before and after implementation. The post-implementation result must match +the baseline: grade F, 4 errors, 3 warnings, 0 silent-starvation risks, and 6 heuristic +uncapped-call matches. No new workflow, prompt, credential, or model-call finding is expected. diff --git a/specs/006-list-launcher-workflow/research.md b/specs/006-list-launcher-workflow/research.md new file mode 100644 index 0000000..9b963ff --- /dev/null +++ b/specs/006-list-launcher-workflow/research.md @@ -0,0 +1,65 @@ +# Research: List Launcher Workflow + +## Keep selection in the command bar + +**Decision**: Move the existing one-based list selection input into the command bar beside List and remove the footer selector. Keep the input visible only when the saved-session list is active and use a compact three-digit width. + +**Rationale**: The current page already exposes the list mode and selection state through `BlogWorkspaceState`; moving the existing interaction avoids a second selection model and keeps related actions together. + +**Alternatives considered**: Keeping both selectors duplicates state and creates conflicting entry points; making every list row a button changes the requested numbered workflow and requires a larger interaction redesign. + +## Use MainTask and CurrentSubTask for restoration + +**Decision**: Restore `ResearchState.MainTask` into New writing prompt and non-empty `ResearchState.CurrentSubTask` into Revision request. If `CurrentSubTask` is empty, leave Revision request empty. + +**Rationale**: These fields already represent the persisted original topic and latest follow-up refinement. Reusing them satisfies the accepted clarification without adding a persisted revision-request property or Cosmos migration. + +**Alternatives considered**: Adding a new revision-request field increases schema and compatibility scope; leaving the field empty discards existing saved refinement context. + +## Launch through the existing initial operation + +**Decision**: After a valid selection is loaded, clear visible Draft, Reviewer notes, and prompt fields, restore the selected prompt values, then invoke the existing initial-prompt submission path exactly once using the restored `MainTask`. + +**Rationale**: The user explicitly requested behavior equivalent to entering the restored new prompt and pressing Enter. Reusing the existing initial operation preserves word-count snapshots, cancellation, output logging, ownership, and workflow termination. + +**Alternatives considered**: Calling a new direct workflow method duplicates submission behavior; invoking a revision operation would incorrectly require an active stable session and change persistence semantics. + +## Keep invalid selection local and non-operative + +**Decision**: Trim input, require a positive integer within the currently displayed list bounds, and reject invalid values before session load or workflow start. Publish the error through the existing workflow log/validation surface. + +**Rationale**: The selection is a UI position, not a session identifier. Local validation prevents unnecessary storage access and guarantees malformed input cannot trigger processing. + +**Alternatives considered**: Passing arbitrary IDs to the store leaks an internal identity boundary and produces less useful errors; browser-only numeric validation does not cover range bounds or whitespace consistently. + +## Use a dedicated Help dialog with clipboard fallback + +**Decision**: Add a small question-mark command that opens an accessible modal dialog containing the fixed HTTPS launch command and a copy action. Use existing JavaScript interop for clipboard access; preserve the visible command and show a copy-failure message when clipboard access is unavailable. + +**Rationale**: A dedicated dialog keeps the command discoverable without adding permanent page text, and a fallback state makes the action understandable when browser permissions deny clipboard access. + +**Alternatives considered**: Navigating away to documentation interrupts the workflow; silently attempting clipboard access gives no feedback; adding a new server endpoint is unnecessary for a static command. + +## Bound the log viewport, not retained history + +**Decision**: Set the workflow log container to a height that displays three normal log-entry lines and retains overflow scrolling. + +**Rationale**: The user requested a smaller scrolling window, not deletion of older workflow history. A visual viewport cap preserves observability while giving more room to the writing panes. + +**Alternatives considered**: Deleting older entries loses diagnostic context; an unbounded log can push primary content below the fold. + +## Preserve disabled-state semantics + +**Decision**: Derive Revision request disabled state from the same condition as Revise: no active/list-selected session or processing is disabled; an eligible selected session enables both controls. + +**Rationale**: Sharing the state rule prevents the button and input from disagreeing and keeps existing processing protections intact. + +**Alternatives considered**: Separate flags can drift; leaving the input enabled invites a submission that the disabled command cannot complete. + +## Validation strategy + +**Decision**: Add focused selection/parser and workspace tests, bUnit command/dialog/page tests, and browser checks for compact layout, three-line scrolling, clipboard semantics, and 390 × 844 / 1440 × 900 responsive behavior. Run full core/web regression and compare MAF Doctor before and after. + +**Rationale**: The feature crosses session restoration, asynchronous workflow launch, command layout, JavaScript clipboard behavior, and responsive presentation. Deterministic tests cover state transitions while browser checks cover the visual contract. + +**Alternatives considered**: Manual testing alone cannot reliably prove exactly-one launch or invalid-input non-operation; live Azure tests are unnecessary for the UI/state contract. diff --git a/specs/006-list-launcher-workflow/spec.md b/specs/006-list-launcher-workflow/spec.md new file mode 100644 index 0000000..fde67b1 --- /dev/null +++ b/specs/006-list-launcher-workflow/spec.md @@ -0,0 +1,134 @@ +# Feature Specification: List Launcher Workflow + +**Feature Branch**: `006-list-launcher-workflow` + +**Created**: 2026-09-20 + +**Status**: Draft + +**Input**: User description: "When List is clicked open an entry field next to the list button. Move the buttons to the right of list over to make room. Make the entry field just big enough to hold 3 numbers and remove the entry field at the bottom of the form. When the user enters a valid number from the list clear the draft and Reviewer Notes and clear the revision request and new writing prompt. Then fill the new writing prompt with the prompt from the selected number and, if we have it, fill the revision request as well. Then immediately begin processing those values as if the user had just entered them and hit enter. Also: make the scrolling window with the logging entries smaller (holding only 3 lines at a time). Also, next to the other buttons add a small button with a question mark. When this is pressed, open a dialog box with the command needed to run the application, and copy that text to the clipboard. Also, when the Revise button is disabled the Revision Request entry should be disabled." + +## Clarifications + +### Session 2026-09-20 + +- Q: Should the saved session's `CurrentSubTask` value be used to restore the Revision request field when launching a session from List? → A: Use `CurrentSubTask` as the saved Revision request when it is non-empty. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Launch a Saved Session from the Command Bar (Priority: P1) + +As a blog writer, I can enter a saved-session number beside List and immediately resume that session's prompts and processing without using a second selector at the bottom of the form. + +**Why this priority**: The current saved-session flow requires separate list selection and a later Revise action; the requested behavior makes recall a direct, compact command-bar workflow. + +**Independent Test**: Open List, enter a valid displayed session number, and verify that the selected session's draft and Reviewer notes are cleared before its prompts are restored and processing starts automatically. + +**Acceptance Scenarios**: + +1. **Given** the workspace is not displaying the saved-session list, **when** List is clicked, **then** a compact numeric entry appears beside List and the commands to its right move right to make room. +2. **Given** the saved-session list is visible, **when** the user enters a valid number corresponding to a displayed session, **then** the draft, Reviewer notes, revision request, and new writing prompt are cleared before the selected session's prompt data is restored. +3. **Given** a selected session has an original prompt and a non-empty `CurrentSubTask`, **when** the valid number is entered, **then** the original prompt fills New writing prompt, `CurrentSubTask` fills Revision request, and processing begins immediately as one normal submission. +4. **Given** a selected session has an empty `CurrentSubTask`, **when** it is launched, **then** Revision request remains empty and processing still begins from the restored new writing prompt. +5. **Given** a valid session number is entered, **when** processing starts, **then** the selected session is processed through the existing workflow and word-count/session behavior remains unchanged. + +--- + +### User Story 2 - Use Compact Command Controls (Priority: P1) + +As a blog writer, I can use a compact command bar that keeps List selection beside List, removes the redundant bottom selector, and provides a question-mark help action. + +**Why this priority**: The requested layout reduces visual travel and keeps command-related controls together while adding a discoverable way to copy the run command. + +**Independent Test**: Render the command bar in normal, list, and processing states and verify placement, sizing, removal of the bottom selector, and the help dialog's copy behavior. + +**Acceptance Scenarios**: + +1. **Given** the command bar is visible, **when** the user inspects it, **then** a small question-mark button appears beside the existing commands. +2. **Given** the user presses the question-mark button, **when** the help dialog opens, **then** it shows the command needed to run the application and offers a copy action that places the same text on the clipboard. +3. **Given** List is active, **when** the command bar is rendered, **then** the three-number entry is next to List, commands to the right remain usable, and no session-number entry appears at the bottom of the form. +4. **Given** the entry field is visible, **when** the user types a value, **then** its width is sufficient for three digits and does not expand or overlap neighboring controls. +5. **Given** the user dismisses the help dialog, **when** the workspace remains open, **then** the underlying command bar and workspace state are unchanged. + +--- + +### User Story 3 - Control Processing Inputs Clearly (Priority: P2) + +As a blog writer, I can tell when revision input is unavailable and see a compact workflow log while processing is active. + +**Why this priority**: Clear disabled-state behavior prevents submissions that cannot succeed, and a smaller log preserves room for the primary writing panes. + +**Independent Test**: Render the workspace without an active session, during processing, and after completion; verify Revision request follows Revise availability and the log displays no more than three visible lines at a time. + +**Acceptance Scenarios**: + +1. **Given** Revise is disabled because no saved session is selected, **when** the prompt area is rendered, **then** Revision request is disabled. +2. **Given** Revise becomes enabled for a selected session, **when** the prompt area is rendered, **then** Revision request becomes available unless processing independently disables it. +3. **Given** workflow log entries exceed three lines, **when** the log is displayed, **then** its scrolling viewport shows three lines at a time and remains scrollable for older entries. +4. **Given** the workspace is at the required mobile or desktop viewport, **when** the log, command bar, and panes are displayed, **then** controls remain readable and do not overlap or introduce horizontal scrolling. + +--- + +### Edge Cases + +- List is clicked while there is unsaved prompt, revision, or word-range work; the existing discard confirmation still applies before changing workspace mode. +- The entered list number is empty, non-numeric, zero, negative, decimal, outside the displayed list, or contains surrounding whitespace; no session is launched and a clear validation message remains available. +- A valid number is entered while a workflow is already processing; the current operation is not replaced by a second launch. +- The selected session has no non-empty `CurrentSubTask`; the restored Revision request remains empty. +- The selected session's restored prompt or revision text contains markup-like characters; it is handled as text and not executable markup. +- Clipboard access is unavailable or denied; the command remains visible and the dialog reports that copying did not complete without changing workspace state. +- The help dialog is opened repeatedly or dismissed with keyboard controls; focus and underlying command state remain usable. +- The log contains long entries; the viewport remains capped at three visible lines while older entries remain reachable by scrolling. +- The selected session cannot be loaded or processing fails; the existing error/log behavior remains visible and no unrelated workspace content is lost. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The command bar MUST show a compact numeric session entry beside List when List mode is active. +- **FR-002**: The numeric session entry MUST be sized to accommodate three digits without resizing or overlapping neighboring commands. +- **FR-003**: Commands to the right of List MUST move right to make room for the List entry while remaining visible and operable. +- **FR-004**: The separate session-number entry at the bottom of the form MUST be removed. +- **FR-005**: Entering a valid displayed session number MUST clear the current Draft, Reviewer notes, Revision request, and New writing prompt before restoring selected-session prompt data. +- **FR-006**: After a valid session number is accepted, the system MUST restore the selected session's original prompt to New writing prompt. +- **FR-007**: After a valid session number is accepted, the system MUST restore the selected session's non-empty `CurrentSubTask` value to Revision request, and leave it empty when `CurrentSubTask` is empty. +- **FR-008**: After prompt data is restored from a valid selection, the system MUST immediately begin processing it using the existing writing workflow as if the user had submitted the restored New writing prompt. +- **FR-009**: Invalid, unavailable, or malformed session-number input MUST NOT begin processing and MUST provide a clear validation outcome in the existing workflow output surface. +- **FR-010**: List selection MUST preserve existing session ownership, cancellation, word-count, persistence, and workflow-termination behavior. +- **FR-011**: The command bar MUST include a compact question-mark help button beside the existing commands. +- **FR-012**: Activating the question-mark button MUST open an accessible dialog showing the command needed to run the application. +- **FR-013**: The help dialog MUST provide a copy action that copies the displayed run command to the clipboard. +- **FR-014**: Clipboard failure MUST leave the command visible and communicate that copying did not complete without changing workspace state. +- **FR-015**: The workflow log scrolling viewport MUST display no more than three lines at a time while allowing access to older entries by scrolling. +- **FR-016**: When Revise is disabled, the Revision request input MUST be disabled; when Revise is enabled and no workflow is processing, the Revision request input MUST be enabled. +- **FR-017**: The feature MUST preserve accessible labels, keyboard operation, focus behavior, safe text rendering, and responsive behavior at 390 × 844 and 1440 × 900. +- **FR-018**: The feature MUST include focused tests for valid and invalid List selection, automatic processing, prompt restoration from `MainTask`, optional revision restoration from `CurrentSubTask`, command-bar layout, help-dialog clipboard behavior, disabled revision input, and three-line log scrolling. + +### Key Entities *(include if feature involves data)* + +- **List Selection Input**: The compact numeric value entered beside List to identify one displayed saved session. +- **Restored Session Prompts**: The selected session's `MainTask` and optional `CurrentSubTask` used to populate the prompt inputs before automatic processing. +- **Run Command Help Dialog**: The accessible transient dialog containing the application launch command and copy action state. +- **Three-Line Workflow Log Viewport**: The bounded scrolling presentation of existing workflow log entries. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: In 100% of valid selection tests, entering one displayed session number clears old workspace content, restores the selected prompt data, and starts exactly one processing operation. +- **SC-002**: In 100% of invalid selection tests, no processing operation starts and a user-visible validation outcome is provided. +- **SC-003**: In 100% of command-bar layout tests, List selection appears beside List, the bottom selector is absent, and the right-side commands remain operable. +- **SC-004**: In 100% of help-dialog tests, the displayed run command matches the configured application command and a successful copy places the same text on the clipboard. +- **SC-005**: In 100% of disabled-state tests, Revision request is disabled whenever Revise is disabled and enabled only when the existing processing/session conditions permit revision. +- **SC-006**: At 390 × 844 and 1440 × 900, the command bar, three-digit selection input, three-line log viewport, prompt inputs, and content panes remain readable without overlap or horizontal scrolling. +- **SC-007**: In 100% of regression tests, existing session ownership, cancellation, word-count behavior, workflow termination, and safe text rendering remain unchanged. + +## Assumptions + +- The selected number is the one-based position shown in the current saved-session list, not a persisted session identifier. +- The application run command shown by Help defaults to `dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https` and is treated as text that can be copied. +- Automatic processing uses the restored New writing prompt as the primary submission; an available stored revision request is restored for the active workspace and remains available according to existing revision behavior. +- Existing discard confirmation applies when List is clicked with unsaved workspace state. +- Existing session records already contain `MainTask` and `CurrentSubTask`; a non-empty `CurrentSubTask` supplies the optional Revision request and no new persistence schema is introduced. +- The workflow log's three-line limit is a visual viewport limit, not a limit on retained entries. +- Existing authentication, session ownership, word-count controls, cancellation, workflow topology, and Reviewer notes behavior are reused. diff --git a/specs/006-list-launcher-workflow/tasks.md b/specs/006-list-launcher-workflow/tasks.md new file mode 100644 index 0000000..cc19112 --- /dev/null +++ b/specs/006-list-launcher-workflow/tasks.md @@ -0,0 +1,199 @@ +# Tasks: List Launcher Workflow + +**Input**: Design documents from `specs/006-list-launcher-workflow/` + +**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/list-launcher-ui.md`, and `quickstart.md` + +**Tests**: Included because the feature specification explicitly requires focused selection, automatic-processing, dialog, disabled-state, responsive, and regression tests. + +**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently after the shared selection contract is established. + +## Phase 1: Setup + +**Purpose**: Establish focused test seams and document the feature validation surface. + +- [X] T001 [P] Add list-launcher test data helpers for summaries and sessions containing `MainTask` and optional `CurrentSubTask` in `BlogWriter.Web.Tests/ListLauncherTestHelpers.cs`. +- [X] T002 [P] Add clipboard and Help-dialog JavaScript interop test helpers in `BlogWriter.Web.Tests/HelpDialogTestHelpers.cs` using existing bUnit JSInterop conventions. +- [X] T003 [P] Add the feature's focused commands, launch command, and expected manual scenarios to `specs/006-list-launcher-workflow/quickstart.md`. + +--- + +## Phase 2: Foundational Workspace Contract + +**Purpose**: Establish shared selection validation and workspace state rules before story-specific UI work. + +- [X] T004 [P] Add one-based displayed-list selection parsing and bounds tests in `BlogWriter.Tests/SessionListSelectionTests.cs` for trimming, positive whole numbers, zero/negative/decimal/text rejection, and out-of-range rejection. +- [X] T005 [P] Add transient selector, restored-prompt, Help-dialog, and copy-status state fields to `BlogWriter.Web/Services/BlogWorkspaceState.cs`, preserving existing `HasUnsavedText`, operation-version, word-range, and Reviewer notes state. +- [X] T006 [P] Define the fixed run command and copy-status values in `BlogWriter.Web/Components/RunCommandDialog.razor` or a shared web constant file, using exactly `dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https`. +- [X] T007 Update `BlogWriter.Web/Services/BlogWorkspaceService.cs` with a validated one-based selection entry point that rejects invalid input before `LoadAsync` or workflow start and preserves existing discard/cancellation semantics. +- [X] T008 Update `BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs` with foundational assertions that invalid selection performs zero loads/workflow calls and valid selection uses the existing owner-scoped session service. + +**Checkpoint**: Selection state and validation are deterministic, non-operative for invalid input, and ready for command-bar integration. + +--- + +## Phase 3: User Story 1 - Launch a Saved Session from the Command Bar (Priority: P1) MVP + +**Goal**: Enter a displayed saved-session number beside List, restore `MainTask` and optional `CurrentSubTask`, and start exactly one initial workflow operation. + +**Independent Test**: Open List, enter a valid number, and verify old Draft/Reviewer/prompt state clears, selected prompt values restore, and one processing operation starts; invalid numbers start none. + +### Tests for User Story 1 + +- [X] T009 [P] [US1] Add `BlogWorkspaceService` tests in `BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs` for clearing Draft/Reviewer/prompt state before restoration, `MainTask` restoration, non-empty `CurrentSubTask` restoration, empty `CurrentSubTask`, exactly-one automatic start, and processing-conflict rejection. +- [X] T010 [P] [US1] Add command-bar selection interaction tests in `BlogWriter.Web.Tests/CommandBarTests.cs` for selector visibility after List, one-based displayed numbering, invalid input feedback, and removal of the bottom selector. +- [X] T011 [P] [US1] Add saved-session compatibility assertions in `BlogWriter.Tests/BlogWriterSessionServiceTests.cs` proving automatic launch reuses existing range, cancellation, persistence, and owner-scoped session behavior. + +### Implementation for User Story 1 + +- [X] T012 [US1] Add the compact List selector and its one-based input binding beside List in `BlogWriter.Web/Components/CommandBar.razor`, retaining New, List, Revise, and Quit callbacks and exposing an accessible three-digit field. +- [X] T013 [US1] Update `BlogWriter.Web/Components/Pages/Home.razor` to pass selector state/actions into `CommandBar`, remove the bottom `session-selector` markup, and preserve existing discard-dialog and workflow-log placement. +- [X] T014 [US1] Implement valid-selection restoration and automatic initial submission in `BlogWriter.Web/Services/BlogWorkspaceService.cs`: clear Draft, Reviewer notes, Revision request, and New writing prompt; load the selected session; restore `MainTask` and non-empty `CurrentSubTask`; invoke the existing initial submission exactly once. +- [X] T015 [US1] Update `BlogWriter.Web/Services/BlogWorkspaceState.cs` to expose selection visibility and restored prompt values without changing persisted `BlogSession` or `ResearchState` schemas. +- [X] T016 [US1] Update `BlogWriter.Web/wwwroot/app.css` to place the three-digit selector beside List, shift right-side commands without overlap, and remove obsolete bottom-selector layout rules at 390 × 844 and 1440 × 900. +- [X] T017 [US1] Run US1 tests from `BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs`, `BlogWriter.Web.Tests/CommandBarTests.cs`, and `BlogWriter.Tests/BlogWriterSessionServiceTests.cs`; verify one valid selection launches one operation and invalid selections launch none. + +**Checkpoint**: User Story 1 is independently demonstrable as the saved-session launcher MVP. + +--- + +## Phase 4: User Story 2 - Use Compact Command Controls (Priority: P1) + +**Goal**: Add the question-mark Help action with an accessible dialog and clipboard copy while preserving command-bar behavior. + +**Independent Test**: Open Help, verify the exact HTTPS run command, copy it successfully, exercise copy failure, dismiss the dialog, and confirm workspace state is unchanged. + +### Tests for User Story 2 + +- [X] T018 [P] [US2] Add command-bar tests in `BlogWriter.Web.Tests/CommandBarTests.cs` for the compact question-mark button, accessible name, List-selector placement, and right-side command operability. +- [X] T019 [P] [US2] Add Help dialog tests in `BlogWriter.Web.Tests/RunCommandDialogTests.cs` for exact command text, modal labeling, successful clipboard copy, clipboard failure feedback, repeated open/close, and unchanged workspace state. +- [X] T020 [P] [US2] Add page integration assertions in `BlogWriter.Web.Tests/HomePageTests.cs` proving the bottom session selector is absent and Help can be opened from the workspace command bar. + +### Implementation for User Story 2 + +- [X] T021 [US2] Create `BlogWriter.Web/Components/RunCommandDialog.razor` with accessible modal semantics, the exact HTTPS launch command, Copy and close actions, visible copy success/failure feedback, and existing focus/JS interop conventions. +- [X] T022 [US2] Update `BlogWriter.Web/Components/CommandBar.razor` to render the compact `?` Help button and raise the Help dialog callback without altering existing command callbacks. +- [X] T023 [US2] Update `BlogWriter.Web/Components/Pages/Home.razor` to own Help-dialog visibility, render `RunCommandDialog`, and preserve underlying workspace state when the dialog opens or closes. +- [X] T024 [US2] Update `BlogWriter.Web/wwwroot/workspace.js` and `BlogWriter.Web/wwwroot/app.css` for clipboard fallback, modal focus/dismissal behavior, compact Help-button styling, and command-bar responsiveness. +- [X] T025 [US2] Run US2 command-bar, dialog, and page tests from `BlogWriter.Web.Tests/CommandBarTests.cs`, `BlogWriter.Web.Tests/RunCommandDialogTests.cs`, and `BlogWriter.Web.Tests/HomePageTests.cs`. + +**Checkpoint**: User Stories 1 and 2 provide the compact List launcher and Help/copy command-bar experience. + +--- + +## Phase 5: User Story 3 - Control Processing Inputs Clearly (Priority: P2) + +**Goal**: Keep Revision request availability synchronized with Revise and constrain the workflow log to a three-line visible viewport while retaining scrollable history. + +**Independent Test**: Render no-session, eligible-session, and processing states; verify Revision request disabled state matches Revise and the log shows three lines while older entries remain scrollable. + +### Tests for User Story 3 + +- [X] T026 [P] [US3] Add prompt-state tests in `BlogWriter.Web.Tests/HomePageTests.cs` proving Revision request is disabled whenever Revise is disabled and enabled only for an eligible non-processing session. +- [X] T027 [P] [US3] Add workflow-log viewport assertions in `BlogWriter.Web.Tests/WorkflowLogTests.cs` for three visible normal entry lines, retained overflow scrolling, and safe text rendering. +- [X] T028 [P] [US3] Extend `BlogWriter.Web.Tests/WorkspaceBrowserTests.cs` for selector width, command-bar non-overlap, three-line log viewport, and no horizontal scrolling at 390 × 844 and 1440 × 900. + +### Implementation for User Story 3 + +- [X] T029 [US3] Update `BlogWriter.Web/Components/Pages/Home.razor` and `BlogWriter.Web/Components/PromptInput.razor` so Revision request `Disabled` follows `!Workspace.State.IsReviseEnabled || Workspace.State.IsProcessing` while preserving Enter/Shift+Enter behavior. +- [X] T030 [US3] Update `BlogWriter.Web/wwwroot/app.css` to cap `.workflow-log` at three normal entry lines, retain overflow scrolling, and preserve readable command/prompt/pane layout on mobile and desktop. +- [X] T031 [US3] Update `BlogWriter.Web/Components/WorkflowLog.razor` only as needed to preserve chronological retained entries, accessible live semantics, and scrollable text-safe output under the three-line viewport. +- [X] T032 [US3] Run US3 component and browser-contract tests from `BlogWriter.Web.Tests/HomePageTests.cs`, `BlogWriter.Web.Tests/WorkflowLogTests.cs`, and `BlogWriter.Web.Tests/WorkspaceBrowserTests.cs`. + +**Checkpoint**: All requested command, input, log, and responsive behaviors are independently validated. + +--- + +## Phase 6: Polish and Cross-Cutting Validation + +**Purpose**: Document the new interaction and verify full compatibility and unchanged MAF health. + +- [X] T033 [P] Update `README.md` and `docs/configuration.md` with the command-bar List launcher, `MainTask`/`CurrentSubTask` restoration, Help command/copy behavior, disabled Revision request, and three-line log viewport. +- [X] T034 [P] Update `specs/006-list-launcher-workflow/quickstart.md` with focused test results and any browser/manual validation outcomes. +- [X] T035 Run the full core and web test suites and build `BlogWriter.csproj` plus `BlogWriter.Web/BlogWriter.Web.csproj` using the commands in `specs/006-list-launcher-workflow/quickstart.md`. +- [X] T036 Run `git diff --check` and inspect changed files for bottom-selector remnants, unsafe markup rendering, duplicate workflow launches, clipboard regressions, and unrelated changes. +- [X] T037 Run MAF Doctor and compare with the baseline: F, 4 errors, 3 warnings, 0 silent-starvation risks, and 6 heuristic uncapped-call matches; investigate any new finding. +- [X] T038 Run every manual quickstart scenario, including invalid/valid selection, automatic launch, optional CurrentSubTask restoration, Help copy success/failure, disabled Revision request, three-line scrolling, and both required viewport sizes. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: T001-T003 can run in parallel immediately. +- **Foundational (Phase 2)**: T004-T008 depends on Setup and blocks all user stories; T004-T006 can run in parallel, followed by T007-T008. +- **User Story 1 (Phase 3)**: Depends on the foundational selection contract and is the MVP; complete before broad UI polish. +- **User Story 2 (Phase 4)**: Depends on the command-bar surface from US1; dialog tests and implementation can proceed after the command-bar callback contract is defined. +- **User Story 3 (Phase 5)**: Depends on the rendered command bar and workflow log from US1/US2; its disabled-state tests can begin after the shared workspace state is available. +- **Polish (Phase 6)**: Depends on all selected stories and focused tests. + +### User Story Dependencies + +- **User Story 1 (P1)**: Requires Phase 2; no dependency on US2 or US3. MVP candidate. +- **User Story 2 (P1)**: Requires Phase 2 and the US1 command-bar integration; independently testable as a Help/copy increment. +- **User Story 3 (P2)**: Requires the existing workflow log and command bar; validates shared layout and disabled-state behavior. + +### Parallel Opportunities + +- Phase 1: T001, T002, and T003 can run in parallel. +- Phase 2: T004, T005, and T006 can run in parallel; T007-T008 follow the shared state contract. +- US1: T009-T011 can run in parallel; T012-T013 can follow the test contract; T016 can proceed alongside service work after layout expectations are set. +- US2: T018-T020 can run in parallel; T021 and T024 can proceed in parallel after the dialog contract is fixed. +- US3: T026-T028 can run in parallel; T029-T031 can be split by page/input, CSS, and component responsibilities. +- Phase 6: T033, T034, and T036 can run in parallel; T035, T037, and T038 are final validation tasks. + +## Parallel Execution Examples + +### User Story 1 + +```text +Task T009: Add workspace restoration and exactly-one-launch tests in BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs +Task T010: Add command-bar selector tests in BlogWriter.Web.Tests/CommandBarTests.cs +Task T011: Add session-service compatibility tests in BlogWriter.Tests/BlogWriterSessionServiceTests.cs +``` + +### User Story 2 + +```text +Task T018: Add compact command-bar tests in BlogWriter.Web.Tests/CommandBarTests.cs +Task T019: Add RunCommandDialog tests in BlogWriter.Web.Tests/RunCommandDialogTests.cs +Task T020: Add Home page Help/bottom-selector tests in BlogWriter.Web.Tests/HomePageTests.cs +``` + +### User Story 3 + +```text +Task T026: Add Revision request disabled-state tests in BlogWriter.Web.Tests/HomePageTests.cs +Task T027: Add three-line WorkflowLog tests in BlogWriter.Web.Tests/WorkflowLogTests.cs +Task T028: Add responsive browser-contract tests in BlogWriter.Web.Tests/WorkspaceBrowserTests.cs +``` + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Setup and Foundational phases. +2. Complete US1 List launcher and automatic session processing. +3. Run US1 focused tests and manually verify one valid selection starts one operation. +4. Stop for an MVP demonstration before adding Help/copy and viewport polish. + +### Incremental Delivery + +1. Establish selection validation and transient workspace state. +2. Deliver US1 saved-session launcher. +3. Deliver US2 compact command bar and Help/copy dialog. +4. Deliver US3 disabled Revision request and three-line log viewport. +5. Complete documentation, full regression, browser checks, and MAF comparison. + +## Notes + +- Every task uses `- [ ] T### [P?] [US#?] description` and names an exact file path. +- Tests are written before their corresponding implementation tasks and must fail for the new behavior before implementation is considered complete. +- No task changes hosted-agent deployment, credentials, token budgets, Cosmos schema, session ownership, or workflow topology. + +## Phase 7: Convergence + +- [X] T039 [P] Add the planned clipboard JS interop helper in `BlogWriter.Web.Tests/HelpDialogTestHelpers.cs` and extend `BlogWriter.Web.Tests/RunCommandDialogTests.cs` to exercise clipboard failure feedback as required by FR-013–FR-014 (missing). +- [X] T040 [P] Replace the placeholder viewport-only assertions in `BlogWriter.Web.Tests/WorkspaceBrowserTests.cs` with executable checks for three-digit selector width, command-bar non-overlap, three-line workflow-log containment, and no horizontal overflow at 390 × 844 and 1440 × 900 per T028, FR-017, and SC-006 (partial). +- [X] T041 Add a deterministic saved-session fixture for browser validation in `BlogWriter.Web.Tests/WorkspaceBrowserTests.cs` or the Testing host setup, seed at least one session with `MainTask` and optional `CurrentSubTask`, and exercise the complete valid List-selection launch flow required by T038 and US1/AC1–AC5 (partial). diff --git a/specs/007-revision-status-controls/checklists/requirements.md b/specs/007-revision-status-controls/checklists/requirements.md new file mode 100644 index 0000000..7e16da3 --- /dev/null +++ b/specs/007-revision-status-controls/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Revision Status Controls + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-09-20 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Revision availability preserves existing processing protections. +- The status-line replacement is scoped to transient workflow presentation; Reviewer notes remain separate. +- Command-bar changes are limited to spacing, sizing, and conditional List-field layout. diff --git a/specs/007-revision-status-controls/contracts/revision-status-ui.md b/specs/007-revision-status-controls/contracts/revision-status-ui.md new file mode 100644 index 0000000..da905f5 --- /dev/null +++ b/specs/007-revision-status-controls/contracts/revision-status-ui.md @@ -0,0 +1,26 @@ +# Revision Status UI Contract + +This is an in-process UI/state contract. No public endpoint or persisted document shape is added. + +## Revision Controls + +- New state enables the Revision request input but not Revise. +- A displayed draft/session enables both Revision request and Revise when not processing. +- Processing disables the Revision request input and Revise action. +- No active draft/session keeps both disabled unless the New-state field rule applies. +- Existing Enter/Shift+Enter behavior remains unchanged. + +## Current Status + +- Render one labeled status region instead of the scrolling workflow-log list. +- Show the newest accepted lifecycle/status message only. +- Replace the prior message on each accepted update. +- Preserve polite live-update semantics and safe text rendering. +- Keep Reviewer notes in its separate pane. + +## Command Bar + +- With the List selector hidden, commands remain grouped. +- With the List selector visible, Revise, Quit, and Help move right to make room. +- All buttons use smaller shared dimensions while retaining readable labels, focus outlines, and operable hit targets. +- Layout remains non-overlapping at 390 × 844 and 1440 × 900. diff --git a/specs/007-revision-status-controls/data-model.md b/specs/007-revision-status-controls/data-model.md new file mode 100644 index 0000000..121a1c8 --- /dev/null +++ b/specs/007-revision-status-controls/data-model.md @@ -0,0 +1,52 @@ +# Data Model: Revision Status Controls + +No persisted schema change is required. The feature adds transient workspace projections for revision availability and the latest workflow status. + +## Revision Availability State + +| Field | Type | Rules | +| --- | --- | --- | +| RevisionInputEnabled | Boolean | True after New or when a draft/session is displayed, unless processing is active | +| ReviseActionEnabled | Boolean | True only when a draft/session exists and processing is inactive | +| HasDraft | Boolean | Derived from non-empty displayed Draft | +| HasNewRevisionContext | Boolean | True for a New workspace ready for input; does not by itself enable Revise | +| IsProcessing | Boolean | Existing processing guard; disables both revision input and action | + +## Current Workflow Status + +| Field | Type | Rules | +| --- | --- | --- | +| Message | String or null | Newest accepted lifecycle/validation/status text; rendered as safe text | +| Outcome | Workflow output outcome or null | Corresponds to the newest lifecycle update | +| IsLive | Boolean | Exposed through an accessible polite live region | + +Only one status message is rendered. New accepted lifecycle updates replace the prior +message. The internal update source may remain ordered for service behavior, but the UI +must not render the collection as a scrolling list. + +## Compact Command Bar + +| State | Layout | +| --- | --- | +| List selector hidden | New, List, Revise, Quit, and Help remain grouped together | +| List selector visible | List and selector retain their relationship; Revise, Quit, and Help move right into the remaining command group | +| Processing | Existing disabled command/input behavior remains in force | + +## State Transitions + +| From | Event | Result | +| --- | --- | --- | +| New | Initialize after accepted New | RevisionInputEnabled true; ReviseActionEnabled false; status unchanged/cleared per existing reset | +| New | Draft completes | RevisionInputEnabled true; ReviseActionEnabled true; latest status shown | +| Draft/session | Revision starts | Revision input and action disabled while processing | +| Processing | Lifecycle update | Replace Current Workflow Status with newest message | +| Any active state | New/List/Quit | Preserve existing discard confirmation and cancellation behavior | +| Any state | Reviewer update | Reviewer notes change independently; Current Workflow Status is not used as review content | + +## Validation Rules + +- Status messages are rendered as text, never markup. +- Empty status uses a compact accessible empty state. +- Revision request cannot submit while processing. +- Revise cannot submit without a draft/session. +- Command layout must not introduce horizontal overflow at 390 × 844 or 1440 × 900. diff --git a/specs/007-revision-status-controls/plan.md b/specs/007-revision-status-controls/plan.md new file mode 100644 index 0000000..7f7ae79 --- /dev/null +++ b/specs/007-revision-status-controls/plan.md @@ -0,0 +1,110 @@ +# Implementation Plan: Revision Status Controls + +**Branch**: `007-revision-status-controls` | **Date**: 2026-09-20 | **Spec**: [spec.md](spec.md) + +**Input**: Feature specification from `/specs/007-revision-status-controls/spec.md` + +**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow. + +## Summary + +Decouple Revision request availability from List mode so New-state users can edit a +revision request while Revise becomes actionable only after a draft/session exists. +Replace the retained scrolling workflow-log list with a single latest-status projection, +while preserving Reviewer notes separately. Add compact command-bar sizing and a +conditional layout that reserves space for the List selector by moving Revise, Quit, and +Help to the right when the selector is visible. + +## Technical Context + +**Language/Version**: C# / .NET 10, Razor components, HTML, and CSS + +**Primary Dependencies**: Existing Interactive Server Blazor host, `BlogWorkspaceState`, +`BlogWorkspaceService`, `PromptInput`, `CommandBar`, `WorkflowLog`, xUnit, bUnit, and +browser checks; no new runtime package + +**Storage**: No persisted schema change. Latest status is transient workspace state; +Reviewer notes and workflow/session persistence remain unchanged. + +**Testing**: Focused bUnit state/component tests, responsive browser-contract checks, +full core/web regression, and accessibility validation + +**Target Platform**: Existing server-hosted responsive Blazor application at 390 × 844 +and 1440 × 900 + +**Project Type**: Existing Interactive Server Blazor web host and web test project + +**Performance Goals**: Each accepted workflow update replaces one in-memory status value +and triggers one normal workspace render; no extra persistence or model call + +**Constraints**: New enables Revision request but not Revise until a draft/session exists; +processing disables submissions; only newest status is rendered; Reviewer notes remain +separate; command buttons get smaller; List selector movement must preserve responsive +layout and WCAG 2.2 AA; no workflow topology or session contract changes + +**Scale/Scope**: Existing workspace command bar, prompt strip, and workflow status only; +no new persistence, API, hosted agent, or deployment resource + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +**Pre-design gate: PASS. Post-design gate: PASS.** + +| Principle | Plan response | Status | +| --- | --- | --- | +| Hosted-Agent Boundaries | UI/state projection only; no hosted-agent or raw HTTP changes. | Pass | +| MAF-Native Workflow Composition | Existing workflow execution and bounded revision loop remain unchanged. | Pass | +| Identity, Secrets, and Budget Control | No credentials, secrets, model calls, or token-budget changes. | Pass | +| Testable and Observable Behavior | State transitions, latest-status replacement, component semantics, and responsive layout receive focused tests. | Pass | +| Simple, Compatible Evolution | Reuses existing workspace update notifications and Reviewer notes; removes only transient log-list presentation. | Pass | + +No MAF scanner change is expected because the feature does not alter agent calls or +workflow topology. + +## Project Structure + +### Documentation (this feature) + +```text +specs/007-revision-status-controls/ +├── plan.md # This file (/speckit-plan command output) +├── research.md # Phase 0 output (/speckit-plan command) +├── data-model.md # Phase 1 output (/speckit-plan command) +├── quickstart.md # Phase 1 output (/speckit-plan command) +├── contracts/ # Phase 1 output (/speckit-plan command) +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) +``` + +### Source Code (repository root) + +```text +./ +├── BlogWriter.Web/ +│ ├── Components/Pages/Home.razor # Bind revision availability and latest status +│ ├── Components/PromptInput.razor # Preserve input semantics and disabled state +│ ├── Components/CommandBar.razor # Compact buttons and conditional grouping +│ ├── Components/WorkflowLog.razor # Render one current status line +│ ├── Services/BlogWorkspaceState.cs # Revision flags and current status projection +│ ├── Services/BlogWorkspaceService.cs # Replace status on accepted updates +│ └── wwwroot/app.css # Compact commands and responsive layout +├── BlogWriter.Web.Tests/ +│ ├── HomePageTests.cs # Revision/New/status behavior +│ ├── CommandBarTests.cs # Grouping and button dimensions +│ ├── WorkflowLogTests.cs # Latest-status rendering +│ └── WorkspaceBrowserTests.cs # Responsive/accessibility contracts +└── README.md / docs/configuration.md # User-facing behavior documentation +``` + +**Structure Decision**: Keep latest-status projection and revision availability in the +existing circuit-scoped workspace state/service. Keep command and status rendering in +the existing Razor components and CSS. Do not change session persistence, Reviewer notes, +workflow routing, or hosted-agent boundaries. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| None | N/A | The feature fits existing workspace state, components, and test boundaries. | diff --git a/specs/007-revision-status-controls/quickstart.md b/specs/007-revision-status-controls/quickstart.md new file mode 100644 index 0000000..94a6125 --- /dev/null +++ b/specs/007-revision-status-controls/quickstart.md @@ -0,0 +1,55 @@ +# Quickstart: Revision Status Controls + +## Prerequisites + +- Existing BlogWriter solution and web test project +- .NET 10 SDK +- No live Foundry, Cosmos, or Entra dependency for focused tests +- Read [revision-status-ui.md](contracts/revision-status-ui.md) for the UI contract + +## Focused automated validation + +From the repository root: + +```powershell +dotnet test BlogWriter.Web.Tests/BlogWriter.Web.Tests.csproj --filter "FullyQualifiedName~HomePageTests|FullyQualifiedName~BlogWorkspaceServiceTests|FullyQualifiedName~WorkflowLogTests|FullyQualifiedName~CommandBarTests" +``` + +Expected outcome: New enables Revision request but not Revise, drafts enable both, +processing disables submissions, the latest status replaces earlier messages, Reviewer +notes remain separate, and command controls retain compact responsive layout. + +## Manual workspace validation + +1. Start the web application with `dotnet run --project BlogWriter.Web/BlogWriter.Web.csproj --launch-profile https` and sign in. +2. In an empty workspace, confirm Revision request and Revise are disabled. +3. Click New; confirm Revision request becomes editable while Revise remains disabled until a draft exists. +4. Submit a prompt and confirm both Revision request and Revise become available after the draft appears. +5. Start a revision and confirm both controls are protected during processing. +6. Trigger progress, success, validation, cancellation, and failure updates; confirm one status line shows only the newest message. +7. Confirm the scrolling workflow-log list is gone and Reviewer notes remain in their own pane. +8. Click List and confirm the List selector appears while Revise, Quit, and Help move right without overlap. +9. Confirm command buttons are smaller but readable and operable. + +## Responsive and accessibility validation + +At 390 × 844 and 1440 × 900: + +- Command buttons and conditional List selector do not overlap or create horizontal scrolling. +- Status line is labeled, polite, text-safe, and readable. +- Revision request disabled state is announced correctly. +- Reviewer notes remain separately labeled and visible. +- Keyboard focus and Enter/Shift+Enter prompt behavior remain intact. + +## Full regression + +```powershell +dotnet test BlogWriter.Tests/BlogWriter.Tests.csproj +dotnet test BlogWriter.Web.Tests/BlogWriter.Web.Tests.csproj +dotnet build BlogWriter.csproj +dotnet build BlogWriter.Web/BlogWriter.Web.csproj +git diff --check +``` + +Existing authentication, session ownership, word-count controls, cancellation, workflow +termination, List selection, Help copy behavior, and Reviewer notes routing must remain unchanged. diff --git a/specs/007-revision-status-controls/research.md b/specs/007-revision-status-controls/research.md new file mode 100644 index 0000000..b054980 --- /dev/null +++ b/specs/007-revision-status-controls/research.md @@ -0,0 +1,41 @@ +# Research: Revision Status Controls + +## Separate Revision request availability from Revise action availability + +**Decision**: Track Revision request availability separately from Revise action availability. New enables the field, a displayed draft/session enables both when not processing, and processing disables both submission paths. + +**Rationale**: The accepted clarification explicitly distinguishes editing the field after New from invoking a revision. The current `IsReviseEnabled` is tied to List mode, so a separate field-enabled rule prevents invalid Revise actions while honoring the requested New behavior. + +**Alternatives considered**: Enabling both on New permits a revision without an active session; keeping both disabled contradicts the requested editable Revision request field; changing session persistence is unnecessary. + +## Project workflow updates to one latest status value + +**Decision**: Retain internal workflow update handling for routing and tests, but expose only the newest lifecycle message as a transient `CurrentStatus` string in workspace state. Reviewer feedback continues to update Reviewer notes separately. + +**Rationale**: The user requested replacing the scrolling presentation, not removing workflow observability or Reviewer notes. A single projection minimizes UI space while preserving the existing update source and state notifications. + +**Alternatives considered**: Deleting the update collection removes useful internal behavior and complicates tests; retaining the rendered list violates the requested presentation; persisting status history is out of scope. + +## Compact command-bar grouping + +**Decision**: Use a normal grouped layout when the List selector is hidden and a selector-aware layout when it is visible. Reduce button dimensions through shared CSS variables/rules while keeping labels, hit targets, focus outlines, and accessible names intact. + +**Rationale**: Conditional layout is the smallest way to move Revise, Quit, and Help right only when List needs space. Shared sizing keeps all commands consistent across modes and viewports. + +**Alternatives considered**: Separate command bars duplicate callbacks and accessibility behavior; absolute positioning risks overlap; shrinking only the List button produces inconsistent controls. + +## Preserve Reviewer notes and submission protections + +**Decision**: Keep Reviewer notes independent from CurrentStatus. Preserve existing operation version, cancellation, word-range validation, and duplicate-submission guards while changing only the state projection and control availability. + +**Rationale**: Reviewer feedback has different user value and retention semantics from lifecycle status. Existing protections are required by the constitution and previous features. + +**Alternatives considered**: Combining status and Reviewer notes loses review history; bypassing existing submission guards can create concurrent workflows. + +## Validation strategy + +**Decision**: Add bUnit tests for empty/draft/New/processing revision states, sequential latest-status replacement, safe text rendering, command grouping/sizing, and Reviewer notes separation. Add responsive browser-contract assertions at 390 × 844 and 1440 × 900, then run full regression and accessibility checks. + +**Rationale**: The feature is concentrated in circuit state and presentation; deterministic component tests cover the state machine, while browser checks cover layout and overflow. + +**Alternatives considered**: Manual-only testing cannot reliably cover transient update replacement or all disabled-state combinations; live workflow tests are unnecessary for this UI projection. diff --git a/specs/007-revision-status-controls/spec.md b/specs/007-revision-status-controls/spec.md new file mode 100644 index 0000000..b9c01e9 --- /dev/null +++ b/specs/007-revision-status-controls/spec.md @@ -0,0 +1,117 @@ +# Feature Specification: Revision Status Controls + +**Feature Branch**: `007-revision-status-controls` + +**Created**: 2026-09-20 + +**Status**: Draft + +**Input**: User description: "Enable the revision field and button when a draft is displayed or the new button is clicked. Remove the scrolling list box that displays the log and replace it with a single line text that updates for each update of the log. Initially keep the buttons together, moving the revise, quit and question mark buttons to the right when the user presses List and the list value box is displayed. That is, have them make room for the list value box. Make all the buttons a little smaller." + +## Clarifications + +### Session 2026-09-20 + +- Q: After clicking New with no active saved session, should Revise be enabled immediately, or only after a new draft has been generated? → A: Enable Revision request after New; enable Revise only after a draft/session exists. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Enable Revision for Drafts (Priority: P1) + +As a blog writer, I can revise whenever a draft is displayed or after choosing New so the Revision request field and Revise command are available at the right time. + +**Why this priority**: Revision is the primary follow-up workflow and should not be blocked by the current list-selection state model. + +**Independent Test**: Render the workspace with no draft, with a draft, after New, and while processing; verify Revise and Revision request states match the intended availability rules. + +**Acceptance Scenarios**: + +1. **Given** no draft is displayed and no eligible session is active, **when** the workspace renders, **then** Revision request and Revise remain disabled. +2. **Given** a draft is displayed, **when** the workspace renders, **then** Revision request and Revise are enabled unless processing is active. +3. **Given** the user clicks New, **when** the new workspace is ready for input, **then** Revision request is enabled but Revise remains disabled until a draft/session exists. +4. **Given** processing is active, **when** the workspace renders, **then** existing processing protections still prevent duplicate submissions. + +--- + +### User Story 2 - See the Latest Status (Priority: P1) + +As a blog writer, I can see the latest workflow status in one compact line without a scrolling log panel taking space from the writing panes. + +**Why this priority**: The requested status presentation is the primary workspace simplification. + +**Independent Test**: Cause successive workflow updates and verify one status line replaces the previous line while remaining visible and accessible. + +**Acceptance Scenarios**: + +1. **Given** no workflow update exists, **when** the workspace renders, **then** a compact empty status appears. +2. **Given** a new workflow update arrives, **when** the workspace refreshes, **then** the single status line displays that newest update. +3. **Given** several updates arrive in sequence, **when** each update is received, **then** only the latest message is visible and no scrolling log list is rendered. +4. **Given** a status message contains long or markup-like text, **when** it is displayed, **then** it remains readable and text-safe without horizontal overflow. + +--- + +### User Story 3 - Keep Commands Compact (Priority: P2) + +As a blog writer, I can use a compact command bar that makes room for the List number field without losing the nearby Revise, Quit, or Help actions. + +**Why this priority**: The command bar must remain usable on narrow screens while accommodating the inline List selector. + +**Independent Test**: Render the command bar before and after List is clicked at mobile and desktop sizes; verify smaller buttons, stable grouping, and right-side command movement. + +**Acceptance Scenarios**: + +1. **Given** List is inactive, **when** the command bar renders, **then** all buttons remain grouped together. +2. **Given** List is clicked and the number field appears, **when** the command bar renders, **then** Revise, Quit, and Help move right to make room. +3. **Given** any required viewport, **when** the compact command bar renders, **then** all controls remain readable, focusable, and non-overlapping. +4. **Given** button labels and the Help symbol are displayed, **when** the controls render, **then** their dimensions are smaller than the previous command-bar dimensions while remaining operable. + +### Edge Cases + +- A draft exists while processing is active; Revision request remains protected from duplicate submission until processing finishes. +- New is clicked while unsaved content exists; existing discard confirmation remains in force before resetting state. +- List is active with a valid or invalid selector; status updates do not displace the selector or command controls. +- A workflow failure, cancellation, validation message, or success message replaces the prior single-line status. +- Long status text wraps or is clipped safely without creating horizontal page overflow. +- Reviewer notes remain in their dedicated pane and are not replaced by the single workflow status line. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: Revision request and Revise MUST be enabled when a draft is displayed and no workflow is processing. +- **FR-002**: Clicking New MUST enable Revision request while leaving Revise disabled until a draft/session exists, unless processing protections apply. +- **FR-003**: Revision request and Revise MUST remain disabled when no draft/session is eligible and no New-state revision context is available. +- **FR-004**: Existing processing protections MUST continue to prevent duplicate revision or initial submissions. +- **FR-005**: The workspace MUST display one current workflow status line instead of the scrolling workflow-log list. +- **FR-006**: Each accepted workflow update MUST replace the displayed status line with its newest message. +- **FR-007**: The status line MUST preserve accessible live-update semantics and render content as safe text. +- **FR-008**: The workflow status line MUST not create horizontal overflow at 390 × 844 or 1440 × 900. +- **FR-009**: When List is inactive, command buttons MUST remain grouped together. +- **FR-010**: When the List number field is visible, Revise, Quit, and Help MUST move to the right to make room without overlap. +- **FR-011**: All command buttons MUST use smaller dimensions than the current command-bar buttons while remaining readable and operable. +- **FR-012**: Reviewer notes MUST remain separate from the single workflow status line. +- **FR-013**: The feature MUST include focused tests for revision availability, New-state availability, latest-status replacement, safe rendering, command-bar movement, button sizing, and responsive layout. + +### Key Entities *(include if feature involves data)* + +- **Revision Availability State**: Whether Revision request and Revise are enabled based on draft, New-state context, active session, and processing state; New enables only the field, while a draft/session enables both. +- **Current Workflow Status**: The newest accepted workflow update displayed as one accessible text line. +- **Compact Command Bar**: The grouped commands and conditional List selector with smaller control dimensions. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: In 100% of tested draft, New, empty, and processing states, Revision request and Revise match the defined availability rules. +- **SC-002**: In 100% of sequential update tests, only the newest workflow message is visible and no scrolling log list is rendered. +- **SC-003**: At 390 × 844 and 1440 × 900, the command bar, conditional List selector, prompts, status line, and content panes remain readable without overlap or horizontal scrolling. +- **SC-004**: Accessibility checks find no new Level A or AA failures in revision controls, status announcements, labels, keyboard order, or text rendering. +- **SC-005**: Reviewer notes remain visible independently while workflow status updates replace one another. + +## Assumptions + +- “New button clicked” means the New workspace is ready for a revision request field after any required discard confirmation; Revise remains disabled until a draft/session exists. +- Processing remains the highest-priority disable condition for both initial and revision submissions. +- The current status line retains only the latest message; no historical workflow-log persistence is added. +- The existing List selector and Help button remain in the command bar; only their spacing and dimensions change. +- Existing authentication, session ownership, word-count controls, cancellation, workflow termination, and Reviewer notes behavior are reused.