Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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. *@
<FluentButton Appearance="FluentAppearance.Accent" OnClick="SendMessage"
Disabled="@(string.IsNullOrWhiteSpace(MessageText) || ThreadViewModel?.IsExecuting == true || HasNoModels)"
Title="@(HasNoModels ? "No model available — configure one to chat" : ThreadViewModel?.IsExecuting == true ? "Waiting for the current response to finish… (Esc to stop)" : "Send")">
Disabled="@(string.IsNullOrWhiteSpace(MessageText) || HasNoModels)"
Title="@(HasNoModels ? "No model available — configure one to chat" : "Send")">
<FluentIcon Value="@(new Icons.Regular.Size20.Send())" Color="Color.Neutral" />
</FluentButton>
}
Expand Down
11 changes: 5 additions & 6 deletions src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions src/MeshWeaver.Blazor.Portal/Chat/ThreadSidePanelContent.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ThreadComposer>(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));
}
Expand Down
20 changes: 18 additions & 2 deletions src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 <svg> with
a viewBox but NO width/height, injected via MarkupString. That <svg> carries no CSS-isolation
scope attribute, so a scoped rule can't reach it — and with no intrinsic size it renders at the
browser default (~300×150) and overflows / collapses, so the icon looked blank or generic.
Constrain any inline avatar SVG to fill the avatar box. */
::deep .mesh-node-picker-avatar svg {
width: 100%;
height: 100%;
display: block;
}

.mesh-node-picker-info {
flex: 1;
min-width: 0;
Expand Down Expand Up @@ -154,7 +165,12 @@
left: 0;
right: 0;
bottom: 0;
z-index: 9;
/* Floating tier (above app chrome). The picker lives inside the chat composer, inside the side
panel whose header is z-index:10 (and the portal menu bar at z-index:1100). At the old
z-index:9/10 the upward-opening dropdown painted UNDER the side-panel top bar (close button
etc.) — the reported overlap. Lift the overlay + dropdown to the same floating tier the
slash-command palette uses so they render ABOVE all chrome. */
z-index: 9999;
background: transparent;
}

Expand All @@ -169,7 +185,7 @@
border: 1px solid var(--neutral-stroke-rest, #d1d1d1);
border-radius: 4px;
box-shadow: var(--elevation-shadow-flyout);
z-index: 10;
z-index: 10000;
padding: 4px;
margin-top: 2px;
}
Expand Down
22 changes: 20 additions & 2 deletions src/MeshWeaver.Blazor/Components/Monaco/MonacoEditorView.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,31 @@ export function initEditor(editorId, placeholder, dotNetRef, codeEditMode = fals
// the editor out when the container height changes. Debounced so rapid typing doesn't
// thrash the DOM; shrink-back on delete works the same way (smaller contentHeight).
if (autoGrow) {
// 🚨 Grow the OUTER container (.monaco-editor-container), NOT the inner
// #editorId element. #editorId IS the .monaco-editor-view div, which
// MonacoEditorView.razor.css pins to `height: 100% !important` — and a CSS
// `!important` rule overrides an inline style, so setting `.style.height`
// on it was SILENTLY IGNORED. The composer therefore stayed stuck at the
// 80px min-height and long input scrolled invisibly (lines "moved up").
// The outer container carries the min-height/max-height clamps and has no
// !important height rule, so sizing IT lets the inner 100% fill it: the box
// expands with the content and only scrolls once max-height is reached.
editorInstance.onDidContentSizeChange((e) => {
const st = editorState.get(editorId);
if (!st) return;
if (st.resizeTimeout) clearTimeout(st.resizeTimeout);
st.resizeTimeout = setTimeout(() => {
const c = document.getElementById(editorId);
if (c) c.style.height = e.contentHeight + 'px';
const inner = document.getElementById(editorId);
const outer = inner?.closest('.monaco-editor-container');
// Guard against a dispose race: the editor can be torn down during the 50ms
// debounce window, after which layout() throws "Couldn't find the editor…".
// getDomNode() returns null once the instance is disposed — bail if so.
if (outer && editorInstance.getDomNode()) {
outer.style.height = e.contentHeight + 'px';
// Re-lay out to the new height now; automaticLayout's ResizeObserver
// would otherwise catch up a frame later.
editorInstance.layout();
}
}, 50);
});
}
Expand Down
Loading