[eric] desktop: orphaned fullscreen tile sealed, all chrome hides while fullscreen, native lights defer to card lights, wash is a GPU-cached texture, tile menu clamps at edges, chat embed exclusive to full view, workflows one-click zones

This commit is contained in:
ciregenz
2026-07-27 20:18:39 -07:00
parent cf37c76331
commit e2af99ea91
14 changed files with 85 additions and 20 deletions
@@ -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;
@@ -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 {
@@ -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 `<stop offset='${offset}%' stop-color='${hex}' stop-opacity='${alpha}'/>`;
}).join('');
// x2/y2 approximate the CSS 115deg direction (25 degrees below horizontal).
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' preserveAspectRatio='none'><defs><linearGradient id='w' x1='0%' y1='0%' x2='90%' y2='42%'>${stopEls}</linearGradient></defs><rect width='100' height='100' fill='url(%23w)'/></svg>`;
return `url("data:image/svg+xml,${svg.replace(/#/g, '%23').replace(/'/g, '%27')}")`;
}