diff --git a/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor b/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor
index 37596c90b..e803c7079 100644
--- a/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor
+++ b/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor
@@ -925,11 +925,14 @@ else
}
else
{
- @* Submit is disabled until the thread is idle — the mid-round inbox is off, so
- follow-ups are ingested only after the current round finishes. Esc cancels. *@
+ @* Never block on the running round. A message sent mid-round is accepted immediately
+ (echoed via PendingUserMessages) and drained when the current round finishes — the
+ composer clears on submit and is ready for the next message right away. Esc cancels
+ the in-flight round. Disabled ONLY for genuinely un-sendable states: empty text or
+ no model. *@
+ Disabled="@(string.IsNullOrWhiteSpace(MessageText) || HasNoModels)"
+ Title="@(HasNoModels ? "No model available — configure one to chat" : "Send")">
}
diff --git a/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs b/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs
index 39193c282..b95908b2b 100644
--- a/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs
+++ b/src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs
@@ -868,12 +868,11 @@ private void SendMessage()
if (_isDisposed)
return;
- // 🚫 Submit is gated on the thread being IDLE. The mid-round inbox is disabled —
- // follow-ups are ingested only after the current round finishes — so we don't accept
- // a submit while executing. The Send button is disabled too (see the footer); this
- // guards the Enter-key path. Esc still cancels the in-flight round.
- if (ThreadViewModel?.IsExecuting == true)
- return;
+ // Never block on the running round: an Enter mid-round is accepted immediately and queued
+ // via PendingUserMessages (drained when the round finishes) — the composer clears on submit
+ // and stays usable, so the user is never locked out while a response streams. Esc still
+ // cancels the in-flight round; TryBeginSubmit (in SubmitMessageCore) dedups an accidental
+ // double-submit of the same text within its debounce window.
// No await in the click path — dispatch to Blazor render context and return void.
// All Hub operations use Post + RegisterCallback; all IMeshService operations
diff --git a/src/MeshWeaver.Blazor.Portal/Chat/ThreadSidePanelContent.razor.cs b/src/MeshWeaver.Blazor.Portal/Chat/ThreadSidePanelContent.razor.cs
index 3f6625dea..07f1442b5 100644
--- a/src/MeshWeaver.Blazor.Portal/Chat/ThreadSidePanelContent.razor.cs
+++ b/src/MeshWeaver.Blazor.Portal/Chat/ThreadSidePanelContent.razor.cs
@@ -165,15 +165,34 @@ private void OpenComposerObserver(string path)
.Subscribe(
threadPath => InvokeAsync(() =>
{
- SidePanelState.SetContentPath(threadPath);
- selectedThreadPath = threadPath;
- StateHasChanged();
- // Consume the signal so it doesn't re-fire on the next subscribe.
+ // Consume the signal FIRST so it can't re-fire on the next subscribe or when the
+ // user returns to this page (a lingering OpenThreadPath would re-navigate).
StreamCache.Update(path, n =>
{
var c = n.ContentAs(Hub.JsonSerializerOptions, _logger);
return c?.OpenThreadPath is null ? n : n with { Content = c with { OpenThreadPath = null } };
- }, Hub.JsonSerializerOptions).Subscribe(_ => { }, _ => { });
+ }, Hub.JsonSerializerOptions).Subscribe(_ => { },
+ ex => _logger?.LogWarning(ex,
+ "[ThreadSidePanel] clearing OpenThreadPath failed for {Path} — the signal may re-fire", path));
+
+ // Started from the MAIN/home screen (root URL) → open the new thread FULL-SCREEN: the
+ // ApplicationPage catch-all ("/{*Path}") renders the full Thread view. Anywhere else
+ // (e.g. a "+" new chat started INSIDE the side panel) keep the in-panel behavior.
+ // Strip any query string / fragment first so "/?x=1" or "/#y" still counts as home —
+ // ToBaseRelativePath keeps them, which would otherwise defeat the root check.
+ var relative = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
+ var cut = relative.IndexOfAny(['?', '#']);
+ if (cut >= 0) relative = relative[..cut];
+ relative = relative.TrimEnd('/');
+ if (string.IsNullOrEmpty(relative))
+ {
+ NavigationManager.NavigateTo($"/{threadPath}");
+ return;
+ }
+
+ SidePanelState.SetContentPath(threadPath);
+ selectedThreadPath = threadPath;
+ StateHasChanged();
}),
ex => _logger?.LogDebug(ex, "[ThreadSidePanel] composer observer errored for {Path}", path));
}
diff --git a/src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.css b/src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.css
index 12764c3f2..f63baf3c5 100644
--- a/src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.css
+++ b/src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.css
@@ -61,6 +61,17 @@
background: var(--neutral-layer-3, #f0f0f0) var(--icon-url) no-repeat center / 18px;
}
+/* Inline-SVG avatar. Harnesses (Claude Code, GitHub Copilot) embed their logo as a raw