[aidan] ux/app-builder-dashboard: improve app builder dashboard (#89)

* [aidan] ux: when agent creates app always opens in dashboard

* [aidan] enhancement: app editing ui mimicing browseragents

* [aidan] ui/ux: when building app window appears immediately

* [aidan]: app view spacing dashboard mimicing browser

* [aidan] ux: app error agent loop

* [aidan] fix: remove duplicate logic
This commit is contained in:
Aidan
2026-06-16 03:44:40 -07:00
committed by GitHub
parent 863fd4f9c5
commit d2d0ce74aa
13 changed files with 516 additions and 57 deletions
@@ -38,6 +38,7 @@ export interface ViewCardPosition {
width: number;
height: number;
zOrder: number;
parent_session_id?: string | null;
}
export interface BrowserTab {
@@ -199,6 +200,11 @@ interface Rect {
h: number;
}
interface CardPlacementExclusion {
type: CardType;
id: string;
}
function rectsOverlap(a: Rect, b: Rect): boolean {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
@@ -206,20 +212,25 @@ function rectsOverlap(a: Rect, b: Rect): boolean {
function collectOccupiedRects(
state: DashboardLayoutState,
expandedSessionIds?: string[],
exclude?: CardPlacementExclusion,
): Rect[] {
const expanded = new Set(expandedSessionIds);
const rects: Rect[] = [];
for (const c of Object.values(state.cards)) {
if (exclude?.type === 'agent' && exclude.id === c.session_id) continue;
const h = expanded.has(c.session_id) ? Math.max(EXPANDED_CARD_MIN_H, c.height) : c.height;
rects.push({ x: c.x, y: c.y, w: c.width, h });
}
for (const c of Object.values(state.viewCards)) {
if (exclude?.type === 'view' && exclude.id === c.output_id) continue;
rects.push({ x: c.x, y: c.y, w: c.width, h: c.height });
}
for (const c of Object.values(state.browserCards)) {
if (exclude?.type === 'browser' && exclude.id === c.browser_id) continue;
rects.push({ x: c.x, y: c.y, w: c.width, h: c.height });
}
for (const n of Object.values(state.notes)) {
if (exclude?.type === 'note' && exclude.id === n.note_id) continue;
rects.push({ x: n.x, y: n.y, w: n.width, h: n.height });
}
return rects;
@@ -312,6 +323,36 @@ export function findOpenSpotNear(
return findOpenGridCell(occupiedRects, newW, newH);
}
export function placeInParentColumn(
state: DashboardLayoutState,
parentSessionId: string | null | undefined,
newW: number,
newH: number,
expandedSessionIds?: string[],
exclude?: CardPlacementExclusion,
): { x: number; y: number } {
const rects = collectOccupiedRects(state, expandedSessionIds, exclude);
const parentCard = parentSessionId ? state.cards[parentSessionId] : null;
if (!parentCard) {
return findOpenGridCell(rects, newW, newH);
}
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
const columnCards = [
...Object.values(state.browserCards).filter(
(c) => !(exclude?.type === 'browser' && exclude.id === c.browser_id),
),
...Object.values(state.viewCards).filter(
(c) => !(exclude?.type === 'view' && exclude.id === c.output_id),
),
].filter((c) => Math.abs(c.x - targetX) < 50);
const targetY = columnCards.length > 0
? Math.max(...columnCards.map((c) => c.y + c.height)) + GRID_GAP
: parentCard.y;
return findOpenSpotNear(targetX, targetY, rects, newW, newH);
}
// Reconnect-refetch merge: ADD only the cards the snapshot carries that the
// client is missing (e.g. a spawned browser whose broadcast was lost in a
// socket gap), collision-resolving each against the live layout so a recovered
@@ -522,27 +563,38 @@ const dashboardLayoutSlice = createSlice({
addViewCard(state, action: PayloadAction<{
outputId: string; expandedSessionIds?: string[];
parentSessionId?: string | null;
x?: number; y?: number; width?: number; height?: number;
}>) {
const { outputId, expandedSessionIds, x, y, width, height } = action.payload;
const { outputId, expandedSessionIds, parentSessionId, x, y, width, height } = action.payload;
if (state.viewCards[outputId]) return;
const w = width || DEFAULT_VIEW_CARD_W;
const h = height || DEFAULT_VIEW_CARD_H;
let posX: number, posY: number;
if (x != null && y != null) {
posX = x;
posY = y;
} else {
const rects = collectOccupiedRects(state, expandedSessionIds);
const pos = findOpenGridCell(rects, DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H);
posX = pos.x;
posY = pos.y;
const parentCard = parentSessionId ? state.cards[parentSessionId] : null;
if (parentCard) {
const pos = placeInParentColumn(state, parentSessionId, w, h, expandedSessionIds);
posX = pos.x;
posY = pos.y;
} else {
const rects = collectOccupiedRects(state, expandedSessionIds);
const pos = findOpenGridCell(rects, w, h);
posX = pos.x;
posY = pos.y;
}
}
state.viewCards[outputId] = {
output_id: outputId,
x: posX,
y: posY,
width: width || DEFAULT_VIEW_CARD_W,
height: height || DEFAULT_VIEW_CARD_H,
width: w,
height: h,
zOrder: state.nextZOrder++,
parent_session_id: parentSessionId || null,
};
},
+12 -13
View File
@@ -23,7 +23,7 @@ import {
clearTurnLabel,
} from '../state/agentsSlice';
import { streamStart, streamDelta, streamEnd, clearStreamingForSession } from '../state/streamingSlice';
import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, setBrowserCardPosition, setGlowingBrowserCards, GRID_GAP } from '../state/dashboardLayoutSlice';
import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, placeInParentColumn, setBrowserCardPosition, setGlowingBrowserCards } from '../state/dashboardLayoutSlice';
import { upsertOutput } from '../state/outputsSlice';
import { displaySessionName } from '../state/sessionDisplay';
import { getAuthToken } from '../config';
@@ -762,21 +762,20 @@ class WebSocketManager {
const parentId = data.parent_session_id;
if (parentId) {
const layoutState = store.getState().dashboardLayout;
const parentCard = layoutState.cards[parentId];
if (parentCard) {
const targetX = parentCard.x + parentCard.width + GRID_GAP * 12;
let targetY = parentCard.y;
const columnCards = Object.values(layoutState.browserCards).filter(
(c) => Math.abs(c.x - targetX) < 50 && c.browser_id !== data.browser_card.browser_id,
const browserCard = layoutState.browserCards[data.browser_card.browser_id];
if (layoutState.cards[parentId] && browserCard) {
const pos = placeInParentColumn(
layoutState,
parentId,
browserCard.width,
browserCard.height,
undefined,
{ type: 'browser', id: browserCard.browser_id },
);
if (columnCards.length > 0) {
const lowestBottom = Math.max(...columnCards.map((c) => c.y + c.height));
targetY = lowestBottom + GRID_GAP;
}
store.dispatch(setBrowserCardPosition({
browserId: data.browser_card.browser_id,
x: targetX,
y: targetY,
x: pos.x,
y: pos.y,
}));
store.dispatch(setGlowingBrowserCards({
browserIds: [data.browser_card.browser_id],