dispatch(closeWorkflowsApp())}
onMinimize={() => dispatch(closeWorkflowsApp())}
- onTile={() => dispatch(toggleWorkflowsHubFullscreen())}
+ onTile={(zone) => {
+ if (zone === 'fullscreen' || zone === 'restore') { dispatch(toggleWorkflowsHubFullscreen()); return; }
+ if (isFullscreen) dispatch(toggleWorkflowsHubFullscreen());
+ onTileZone?.(zone);
+ }}
tiled={isFullscreen}
- noTileMenu
+ noTileMenu={isFullscreen}
/>
diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts
index 713c4dfc..dd0b8d6e 100644
--- a/frontend/src/shared/state/dashboardLayoutSlice.ts
+++ b/frontend/src/shared/state/dashboardLayoutSlice.ts
@@ -714,6 +714,9 @@ const dashboardLayoutSlice = createSlice({
if (!liveIds.has(id)) {
state.closedCardPositions[id] = { ...state.cards[id] };
delete state.cards[id];
+ // A dead card must never keep owning a tile: an orphaned 'fullscreen' entry hides ALL chrome until reload.
+ delete state.tiledCards[id];
+ delete state.minimizedCards[id];
}
}
@@ -1900,8 +1903,13 @@ export const reopenLastClosed = createAsyncThunk(
);
export const selectFullscreenCardId = (state: { dashboardLayout: DashboardLayoutState }): string | null => {
- const entry = Object.entries(state.dashboardLayout.tiledCards).find(([, zone]) => zone === 'fullscreen');
- return entry ? entry[0] : null;
+ const s = state.dashboardLayout;
+ const entry = Object.entries(s.tiledCards).find(([, zone]) => zone === 'fullscreen');
+ if (!entry) return null;
+ const id = entry[0];
+ // Belt over the reducer hygiene: an entry whose card is gone (any removal path) must not hold the app in fullscreen.
+ const exists = id in s.cards || id in s.viewCards || id in s.browserCards || id in s.notes || id in s.workflowCards;
+ return exists ? id : null;
};
export default dashboardLayoutSlice.reducer;
diff --git a/frontend/src/shared/state/subscriptionsSlice.ts b/frontend/src/shared/state/subscriptionsSlice.ts
index 25fc5a58..320a1229 100644
--- a/frontend/src/shared/state/subscriptionsSlice.ts
+++ b/frontend/src/shared/state/subscriptionsSlice.ts
@@ -112,15 +112,21 @@ export const { setSubscriptionStatus, markSubscriptionConnected, hideProviderHea
const EMPTY_CONNECTIONS: SubscriptionConnection[] = [];
/** Unwraps the polymorphic `providers` shape (modern object vs legacy array). */
+let p_lastRows: SubscriptionConnection[] | null = null;
+let p_lastFiltered: SubscriptionConnection[] | null = null;
+
export function selectSubscriptionConnections(
state: WithSubscriptions,
): SubscriptionConnection[] {
const providers = state.subscriptions.status?.providers;
if (!providers) return EMPTY_CONNECTIONS;
const rows = Array.isArray(providers) ? providers : (providers.connections ?? EMPTY_CONNECTIONS);
- // The free-trial/Pro lane is an OpenSwarm-managed router row, not a connection the USER made; counting it made a fresh trial look "connected" (dead Connect-beat rows, premature onConnected, phantom green rings).
+ // The free-trial/Pro lane is an OpenSwarm-managed router row, not a connection the USER made; counting it made a fresh trial look "connected" (dead Connect-beat rows, premature onConnected, phantom green rings). Memoized per rows identity so useSelector consumers don't churn.
+ if (rows === p_lastRows && p_lastFiltered) return p_lastFiltered;
const filtered = rows.filter((p) => !/\(OpenSwarm-managed\)/.test(p.name ?? ''));
- return filtered.length === rows.length ? rows : filtered;
+ p_lastRows = rows;
+ p_lastFiltered = filtered.length === rows.length ? rows : filtered;
+ return p_lastFiltered;
}
export function hasAnyActiveSubscription(state: WithSubscriptions): boolean {
diff --git a/frontend/src/shared/styles/washBackground.ts b/frontend/src/shared/styles/washBackground.ts
new file mode 100644
index 00000000..1fd76341
--- /dev/null
+++ b/frontend/src/shared/styles/washBackground.ts
@@ -0,0 +1,14 @@
+// Theme wash as an SVG IMAGE, not a CSS linear-gradient: Chromium caches a decoded image as a GPU
+// texture, while a window-sized procedural gradient re-rasterizes on resize and, under GPU memory
+// pressure (webviews, external monitors), those rasters get DROPPED and paint as a half/blank
+// rectangle (the same class as the 1.5.9 dot-grid white-patch bug; see DashboardCanvas's grid note).
+export function washBackgroundUrl(stops: string[], washOpacity: number): string {
+ const alpha = Math.max(0, Math.min(1, washOpacity));
+ const stopEls = stops.map((hex, i) => {
+ const offset = stops.length > 1 ? (i / (stops.length - 1)) * 100 : 100;
+ return ``;
+ }).join('');
+ // x2/y2 approximate the CSS 115deg direction (25 degrees below horizontal).
+ const svg = ``;
+ return `url("data:image/svg+xml,${svg.replace(/#/g, '%23').replace(/'/g, '%27')}")`;
+}