mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[eric] canvas: un-minimizing keeps your spot when it is free and reflows when it is not
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { restoredCardPosition } from './restoredCardPosition';
|
||||
import { createSlice, createAsyncThunk, PayloadAction, createAction } from '@reduxjs/toolkit';
|
||||
import { launchAndSendFirstMessage, resumeSession, collapseSession, collapseAllSessions, setExpandedSessionIds } from './agentsSlice';
|
||||
import { untileClosedChats } from './untileClosedChats';
|
||||
@@ -631,6 +632,19 @@ export interface SpawnAnchor {
|
||||
viewportCenter?: { x: number; y: number };
|
||||
}
|
||||
|
||||
// Which card map holds this id, plus the exclusion tag `collectOccupiedRects` needs so a card is
|
||||
// never treated as blocking its own restore.
|
||||
function p_placedCard(state: DashboardLayoutState, id: string):
|
||||
{ card: { x: number; y: number; width: number; height: number }; exclude: CardPlacementExclusion } | null {
|
||||
const agent = Object.values(state.cards).find((c) => c.session_id === id);
|
||||
if (agent) return { card: agent, exclude: { type: 'agent', id } };
|
||||
const view = Object.values(state.viewCards).find((c) => c.output_id === id);
|
||||
if (view) return { card: view, exclude: { type: 'view', id } };
|
||||
const browser = Object.values(state.browserCards).find((c) => c.browser_id === id);
|
||||
if (browser) return { card: browser, exclude: { type: 'browser', id } };
|
||||
return null;
|
||||
}
|
||||
|
||||
export function computeSpawnPosition(
|
||||
state: DashboardLayoutState,
|
||||
newW: number,
|
||||
@@ -696,6 +710,19 @@ const dashboardLayoutSlice = createSlice({
|
||||
const id = action.payload.cardId;
|
||||
if (state.minimizedCards[id]) {
|
||||
delete state.minimizedCards[id];
|
||||
// Coming back onto an occupied slot lands the card on top of whatever took its place while
|
||||
// it was parked. Keep the user's spot when it is still free, reflow only when it is not.
|
||||
const owner = p_placedCard(state, id);
|
||||
if (owner) {
|
||||
const occupied = collectOccupiedRects(state, undefined, owner.exclude);
|
||||
const next = restoredCardPosition(
|
||||
{ x: owner.card.x, y: owner.card.y, w: owner.card.width, h: owner.card.height },
|
||||
occupied,
|
||||
findOpenSpotNear,
|
||||
);
|
||||
owner.card.x = next.x;
|
||||
owner.card.y = next.y;
|
||||
}
|
||||
} else {
|
||||
state.minimizedCards[id] = true;
|
||||
if (state.tiledCards[id] === 'fullscreen') delete state.tiledCards[id];
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Run: npm test (frontend/scripts/run-tests.mjs)
|
||||
//
|
||||
// Haik: "expanding a minimized card should use the same best-position placement logic that runs for
|
||||
// every other spawning card" — it respawned into its original slot even when the layout had shifted
|
||||
// under it, so it landed on top of whatever had moved in.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { restoredCardPosition, type Rect } from './restoredCardPosition.ts';
|
||||
|
||||
const CARD: Rect = { x: 100, y: 100, w: 300, h: 200 };
|
||||
// Stand-in for findOpenSpotNear: answers with a fixed marker so a reflow is unmistakable.
|
||||
const SPOT = (x: number, y: number): { x: number; y: number } => ({ x: x + 999, y: y + 999 });
|
||||
|
||||
test('a card whose old spot is still free comes back exactly where it was left', () => {
|
||||
const far: Rect[] = [{ x: 900, y: 900, w: 100, h: 100 }];
|
||||
assert.deepEqual(restoredCardPosition(CARD, far, SPOT), { x: 100, y: 100 });
|
||||
});
|
||||
|
||||
test('a card whose old spot is now taken reflows instead of landing on top', () => {
|
||||
const onTop: Rect[] = [{ x: 150, y: 150, w: 300, h: 200 }];
|
||||
const got = restoredCardPosition(CARD, onTop, SPOT);
|
||||
assert.deepEqual(got, { x: 1099, y: 1099 }, 'restored onto an occupied slot');
|
||||
});
|
||||
|
||||
test('the reflow is anchored on the old position, so the card stays near where it was parked', () => {
|
||||
const onTop: Rect[] = [{ x: 150, y: 150, w: 300, h: 200 }];
|
||||
const calls: number[][] = [];
|
||||
restoredCardPosition(CARD, onTop, (x, y) => { calls.push([x, y]); return { x, y }; });
|
||||
assert.deepEqual(calls, [[100, 100]], 'searched from somewhere other than the parked spot');
|
||||
});
|
||||
|
||||
test('edge contact is not an overlap, so touching cards do not trigger a pointless move', () => {
|
||||
const flush: Rect[] = [{ x: 400, y: 100, w: 100, h: 200 }]; // starts exactly where CARD ends
|
||||
assert.deepEqual(restoredCardPosition(CARD, flush, SPOT), { x: 100, y: 100 });
|
||||
});
|
||||
|
||||
test('an empty board never reflows', () => {
|
||||
assert.deepEqual(restoredCardPosition(CARD, [], SPOT), { x: 100, y: 100 });
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// Where should a card land when you un-minimize it? (Haik, 2026-08-13)
|
||||
//
|
||||
// It used to land exactly where it was parked, which is right when that space is still empty and
|
||||
// wrong when the board moved on without it: the card reappears on top of whatever took its place.
|
||||
//
|
||||
// So: keep the user's spot when it is still free, and only reflow when it is not. Always reflowing
|
||||
// would be its own bug, because a user who parked a card somewhere deliberately expects it back
|
||||
// there, and the nearest-free-spot search is anchored on the old position so even a reflowed card
|
||||
// comes back close to where it was left rather than at the far end of the board.
|
||||
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param current where the card was parked
|
||||
* @param occupied every other card's rect (the restoring card MUST be excluded, or it collides
|
||||
* with its own footprint and always reflows)
|
||||
* @param findSpot nearest-free-spot search, injected so this stays free of the layout slice
|
||||
*/
|
||||
export function restoredCardPosition(
|
||||
current: Rect,
|
||||
occupied: Rect[],
|
||||
findSpot: (x: number, y: number, occupied: Rect[], w: number, h: number) => { x: number; y: number },
|
||||
): { x: number; y: number } {
|
||||
const clash = occupied.some((r) => (
|
||||
current.x < r.x + r.w && current.x + current.w > r.x
|
||||
&& current.y < r.y + r.h && current.y + current.h > r.y
|
||||
));
|
||||
if (!clash) return { x: current.x, y: current.y };
|
||||
return findSpot(current.x, current.y, occupied, current.w, current.h);
|
||||
}
|
||||
Reference in New Issue
Block a user