mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] notes: delete the sticky-note feature end to end
This commit is contained in:
@@ -547,7 +547,6 @@ async def duplicate_dashboard(dashboard_id: str):
|
||||
"cards": new_cards,
|
||||
"view_cards": source_layout.get("view_cards", {}) or {},
|
||||
"browser_cards": new_browser_cards,
|
||||
"notes": source_layout.get("notes", {}) or {},
|
||||
"expanded_session_ids": new_expanded,
|
||||
}
|
||||
|
||||
|
||||
@@ -50,16 +50,6 @@ class BrowserCardPosition(BaseModel):
|
||||
docked_to: Optional[str] = None
|
||||
|
||||
|
||||
class NotePosition(BaseModel):
|
||||
note_id: str
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
width: float = 240
|
||||
height: float = 200
|
||||
content: str = ""
|
||||
color: str = "yellow"
|
||||
|
||||
|
||||
class DashboardLayout(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
cards: dict[str, CardPosition] = Field(default_factory=dict)
|
||||
@@ -67,7 +57,6 @@ class DashboardLayout(BaseModel):
|
||||
browser_cards: dict[str, BrowserCardPosition] = Field(default_factory=dict)
|
||||
workflow_cards: dict = Field(default_factory=dict)
|
||||
workflows_hub: Optional[dict] = None
|
||||
notes: dict[str, NotePosition] = Field(default_factory=dict)
|
||||
expanded_session_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class DashboardExportable:
|
||||
expanded = [b for b in (ctx.bundle_id_for(EntityType.session, s) for s in (layout.get("expanded_session_ids") or [])) if b]
|
||||
return {"name": self.p_data.get("name") or "Dashboard", "layout": {
|
||||
**layout, "cards": cards, "view_cards": view_cards,
|
||||
"browser_cards": browser_cards, "notes": layout.get("notes") or {},
|
||||
"browser_cards": browser_cards,
|
||||
"expanded_session_ids": expanded,
|
||||
}}
|
||||
|
||||
@@ -114,7 +114,7 @@ class DashboardExportable:
|
||||
"updated_at": now,
|
||||
"layout": {
|
||||
**layout, "cards": cards, "view_cards": view_cards,
|
||||
"browser_cards": browser_cards, "notes": layout.get("notes") or {},
|
||||
"browser_cards": browser_cards,
|
||||
"expanded_session_ids": expanded,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
- Message.client_message_id round-trip (optimistic dedupe)
|
||||
- Mode migration: 'chat' -> 'ask' on reconcile + lifespan deletion
|
||||
- DashboardLayout notes round-trip
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -186,62 +185,6 @@ def test_reconcile_idempotent():
|
||||
assert mtime_after_first == mtime_after_second, "reconcile must be idempotent"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Group 6, Notes layout serialization ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_dashboard_layout_notes_round_trip():
|
||||
from backend.apps.dashboards.models import DashboardLayout, NotePosition
|
||||
|
||||
n = NotePosition(note_id="n1", x=100, y=200, content="todo: ship",
|
||||
color="yellow", width=240, height=200)
|
||||
layout = DashboardLayout(notes={"n1": n})
|
||||
dumped = layout.model_dump(mode="json")
|
||||
assert "notes" in dumped
|
||||
assert dumped["notes"]["n1"]["content"] == "todo: ship"
|
||||
|
||||
rehydrated = DashboardLayout.model_validate(dumped)
|
||||
assert rehydrated.notes["n1"].content == "todo: ship"
|
||||
assert rehydrated.notes["n1"].color == "yellow"
|
||||
|
||||
|
||||
def test_dashboard_layout_legacy_no_notes():
|
||||
"""Older dashboard JSON without 'notes' must still load cleanly."""
|
||||
from backend.apps.dashboards.models import DashboardLayout
|
||||
|
||||
legacy = {
|
||||
"cards": {}, "view_cards": {}, "browser_cards": {},
|
||||
"expanded_session_ids": [],
|
||||
}
|
||||
layout = DashboardLayout.model_validate(legacy)
|
||||
assert layout.notes == {}
|
||||
|
||||
|
||||
def test_notes_stress_many_round_trips():
|
||||
"""500 notes with random colors / positions must all serialize."""
|
||||
from backend.apps.dashboards.models import DashboardLayout, NotePosition
|
||||
|
||||
notes = {}
|
||||
colors = ["yellow", "pink", "blue", "green", "purple", "gray"]
|
||||
for i in range(500):
|
||||
nid = f"n{i}"
|
||||
notes[nid] = NotePosition(
|
||||
note_id=nid,
|
||||
x=random.uniform(-5000, 5000),
|
||||
y=random.uniform(-5000, 5000),
|
||||
width=random.uniform(160, 600),
|
||||
height=random.uniform(120, 600),
|
||||
content="x" * random.randint(0, 5000),
|
||||
color=random.choice(colors),
|
||||
)
|
||||
layout = DashboardLayout(notes=notes)
|
||||
dumped = layout.model_dump(mode="json")
|
||||
rehydrated = DashboardLayout.model_validate(dumped)
|
||||
assert len(rehydrated.notes) == 500
|
||||
for nid, orig in notes.items():
|
||||
assert rehydrated.notes[nid].content == orig.content
|
||||
assert rehydrated.notes[nid].color == orig.color
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- Group 7, Concurrent send_message dedupe stress Real-world scenario: user mashes Enter quickly. 50 concurrent sends each with a unique client_message_id must produce 50 echoed messages carrying the right ids. Pure pydantic / asyncio test, no real agent loop. ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ def test_dashboard_export_import_carries_agent_cards_and_transcript(tmp_path, mo
|
||||
"cards": {sid1: {"session_id": sid1}, sid2: {"session_id": sid2}},
|
||||
"view_cards": {},
|
||||
"browser_cards": {bkey: {"browser_id": bkey, "url": "u", "spawned_by": None}},
|
||||
"notes": {}, "expanded_session_ids": [sid1],
|
||||
"expanded_session_ids": [sid1],
|
||||
}}))
|
||||
|
||||
raw, _ = closure.build_bundle(EntityType.dashboard, did)
|
||||
|
||||
@@ -55,18 +55,17 @@ test.describe('combinatorial user flows', () => {
|
||||
await el.click({ timeout: 8_000 });
|
||||
return el;
|
||||
};
|
||||
// The bottom dashboard toolbar (New Agent / Add note / Add App / Browser) only
|
||||
// mounts when a dashboard is active; a clean CI profile has none, so create one
|
||||
// via the sidebar "+". Idempotent: returns early if the toolbar is already up.
|
||||
// The bottom spawn pill only mounts when a dashboard is active; a clean CI profile has none, so create one via the sidebar "+". Idempotent: returns early if the pill is already up.
|
||||
const ensureDashboardActive = async () => {
|
||||
const spawnPill = page.getByText('Ask me anything...', { exact: true });
|
||||
const toggle = page.locator('[data-onboarding="sidebar-toggle"]');
|
||||
if ((await toggle.getAttribute('aria-expanded')) === 'false') await toggle.click({ timeout: 5_000 }).catch(() => {});
|
||||
await clickMust(page.locator('[data-onboarding="sidebar-dashboards"]'), 'sidebar dashboards');
|
||||
if (await page.getByRole('button', { name: 'Add note' }).isVisible().catch(() => false)) return;
|
||||
if (await spawnPill.isVisible().catch(() => false)) return;
|
||||
const createBtn = page.locator('[data-onboarding="sidebar-dashboards"] button').first();
|
||||
if (await createBtn.count()) await createBtn.click({ timeout: 5_000 }).catch(() => {});
|
||||
await expect.poll(() => page.url(), { timeout: 8_000 }).toMatch(/\/dashboard\//);
|
||||
await expect(page.getByRole('button', { name: 'Add note' }), 'dashboard toolbar never mounted').toBeVisible({ timeout: 10_000 });
|
||||
await expect(spawnPill, 'dashboard spawn pill never mounted').toBeVisible({ timeout: 10_000 });
|
||||
};
|
||||
const errorsSince = (mark: number) => errors.slice(mark).filter((e) => !CONSOLE_WHITELIST.some((rx) => rx.test(e.text)));
|
||||
const assertNoNew = (mark: number, label: string) => {
|
||||
@@ -301,12 +300,9 @@ test.describe('combinatorial user flows', () => {
|
||||
assertNoNew(mark, 'Browser card mount (webview)');
|
||||
});
|
||||
|
||||
test('dashboard toolbar: Add note + Add App + History each mount their surfaces', async () => {
|
||||
test('dashboard toolbar: Add App + History each mount their surfaces', async () => {
|
||||
const mark = errors.length;
|
||||
await ensureDashboardActive();
|
||||
await clickMust(page.getByRole('button', { name: 'Add note' }), 'toolbar Add note');
|
||||
assertNoNew(mark, 'Add note mount');
|
||||
|
||||
await clickMust(page.getByRole('button', { name: 'Add App' }), 'toolbar Add App');
|
||||
// Picker is a dialog; closing via Escape is enough.
|
||||
await page.keyboard.press('Escape').catch(() => {});
|
||||
|
||||
@@ -183,13 +183,6 @@ test.describe('deep interactive coverage', () => {
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
test('Add note mounts (sticky)', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'Add note' }) as any, 'Add note');
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: info.outputPath('note.png') });
|
||||
noNewCrashes('Add note mount');
|
||||
});
|
||||
|
||||
test('Add App picker opens', async ({}, info) => {
|
||||
await safeClick(page.getByRole('button', { name: 'Add App' }) as any, 'Add App');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
@@ -41,7 +41,6 @@ interface Props {
|
||||
onAddView: (outputId: string, opts?: { newInstance?: boolean }) => void;
|
||||
onHistoryResume: (sessionId: string) => void;
|
||||
onAddBrowser: () => void;
|
||||
onAddNote: () => void;
|
||||
dashboardId?: string;
|
||||
newAgentBounce?: boolean;
|
||||
canvasEmpty?: boolean;
|
||||
@@ -71,7 +70,7 @@ function formatRelativeTime(dateStr: string | null): string {
|
||||
}
|
||||
|
||||
const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, onAddNote, dashboardId, newAgentBounce, canvasEmpty, onNewAgentBounceEnd, prefillPrompt, prefillMode }, ref) => {
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, dashboardId, newAgentBounce, canvasEmpty, onNewAgentBounceEnd, prefillPrompt, prefillMode }, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const elementSelection = useElementSelection();
|
||||
@@ -608,7 +607,6 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
if (newAgentBounce) onNewAgentBounceEnd?.();
|
||||
onNewAgent();
|
||||
}}
|
||||
onAddNote={onAddNote}
|
||||
onAddBrowser={onAddBrowser}
|
||||
onAddApp={handleOpenViewPicker}
|
||||
onWorkflows={() => dispatch(workflowsHubOpen ? closeWorkflowsApp() : openWorkflowsApp())}
|
||||
|
||||
@@ -20,7 +20,6 @@ import type {
|
||||
CardPosition,
|
||||
ViewCardPosition,
|
||||
BrowserCardPosition,
|
||||
NotePosition,
|
||||
WorkflowCardPosition,
|
||||
WorkflowsHubPosition,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -50,7 +49,6 @@ interface DashboardCanvasProps {
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
keepAliveBrowserCards: Record<string, BrowserCardPosition>;
|
||||
notes: Record<string, NotePosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
outputs: Record<string, Output>;
|
||||
@@ -60,7 +58,6 @@ interface DashboardCanvasProps {
|
||||
highlightedCardId: string | null;
|
||||
autoFocusSessionId: string | null;
|
||||
focusedCardId: string | null;
|
||||
pendingFocusNoteId: string | null;
|
||||
multiDragDelta: { dx: number; dy: number } | null;
|
||||
shakeDirection: Direction | null;
|
||||
neighborDirections: NeighborDirections;
|
||||
@@ -95,7 +92,6 @@ interface DashboardCanvasProps {
|
||||
onAddView: (outputId: string, opts?: { newInstance?: boolean }) => void;
|
||||
onHistoryResume: (sessionId: string) => void;
|
||||
onAddBrowser: () => void;
|
||||
onAddNote: () => void;
|
||||
onNewAgentBounceEnd: () => void;
|
||||
onFitToView: () => void;
|
||||
onTidy: () => void;
|
||||
@@ -114,7 +110,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
viewCards,
|
||||
browserCards,
|
||||
keepAliveBrowserCards,
|
||||
notes,
|
||||
workflowCards,
|
||||
workflowsHub,
|
||||
outputs,
|
||||
@@ -124,7 +119,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
highlightedCardId,
|
||||
autoFocusSessionId,
|
||||
focusedCardId,
|
||||
pendingFocusNoteId,
|
||||
multiDragDelta,
|
||||
shakeDirection,
|
||||
neighborDirections,
|
||||
@@ -159,7 +153,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
onAddView,
|
||||
onHistoryResume,
|
||||
onAddBrowser,
|
||||
onAddNote,
|
||||
onNewAgentBounceEnd,
|
||||
onFitToView,
|
||||
onTidy,
|
||||
@@ -261,7 +254,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
browserCards={browserCards}
|
||||
workflowCards={workflowCards}
|
||||
workflowsHub={workflowsHub}
|
||||
notes={notes}
|
||||
expandedSessionIds={expandedSessionIds}
|
||||
outputs={outputs}
|
||||
dashboardId={dashboardId}
|
||||
@@ -287,7 +279,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
cards={cards}
|
||||
viewCards={viewCards}
|
||||
browserCards={browserCards}
|
||||
notes={notes}
|
||||
workflowCards={workflowCards}
|
||||
outputs={outputs}
|
||||
selectedIds={Array.from(selection.selectedIds.keys())}
|
||||
@@ -297,7 +288,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
}}
|
||||
onApplications={() => setAppsWindowOpen((v) => !v)}
|
||||
onAddBrowser={onAddBrowser}
|
||||
onAddNote={onAddNote}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -399,7 +389,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
viewCards={viewCards}
|
||||
browserCards={browserCards}
|
||||
keepAliveBrowserCards={keepAliveBrowserCards}
|
||||
notes={notes}
|
||||
workflowCards={workflowCards}
|
||||
workflowsHub={workflowsHub}
|
||||
outputs={outputs}
|
||||
@@ -410,7 +399,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
highlightedCardId={highlightedCardId}
|
||||
autoFocusSessionId={autoFocusSessionId}
|
||||
focusedCardId={focusedCardId}
|
||||
pendingFocusNoteId={pendingFocusNoteId}
|
||||
multiDragDelta={multiDragDelta}
|
||||
shakeDirection={shakeDirection}
|
||||
spawnOriginsRef={spawnOriginsRef}
|
||||
@@ -459,7 +447,6 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
onAddView={onAddView}
|
||||
onHistoryResume={onHistoryResume}
|
||||
onAddBrowser={onAddBrowser}
|
||||
onAddNote={onAddNote}
|
||||
onNewAgentBounceEnd={onNewAgentBounceEnd}
|
||||
onFitToView={onFitToView}
|
||||
onTidy={onTidy}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AnimatePresence } from 'framer-motion';
|
||||
import AgentCard from '../cards/AgentCard';
|
||||
import DashboardViewCard from '../cards/DashboardViewCard';
|
||||
import BrowserCard from '../cards/BrowserCard';
|
||||
import NoteCard from '../cards/NoteCard';
|
||||
import WorkflowsAppCard from '@/app/pages/Workflows/app/WorkflowsAppCard';
|
||||
import RunMonitor from '@/app/pages/Workflows/app/RunMonitor';
|
||||
import {
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
type CardPosition,
|
||||
type ViewCardPosition,
|
||||
type BrowserCardPosition,
|
||||
type NotePosition,
|
||||
type WorkflowCardPosition,
|
||||
type WorkflowsHubPosition,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -33,7 +31,6 @@ interface DashboardCardLayerProps {
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
keepAliveBrowserCards: Record<string, BrowserCardPosition>;
|
||||
notes: Record<string, NotePosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
outputs: Record<string, Output>;
|
||||
@@ -44,7 +41,6 @@ interface DashboardCardLayerProps {
|
||||
highlightedCardId: string | null;
|
||||
autoFocusSessionId: string | null;
|
||||
focusedCardId: string | null;
|
||||
pendingFocusNoteId: string | null;
|
||||
multiDragDelta: { dx: number; dy: number } | null;
|
||||
shakeDirection: Direction | null;
|
||||
spawnOriginsRef: RefObject<Record<string, SpawnOrigin>>;
|
||||
@@ -67,7 +63,6 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
|
||||
viewCards,
|
||||
browserCards,
|
||||
keepAliveBrowserCards,
|
||||
notes,
|
||||
workflowCards,
|
||||
workflowsHub,
|
||||
outputs,
|
||||
@@ -78,7 +73,6 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
|
||||
highlightedCardId,
|
||||
autoFocusSessionId,
|
||||
focusedCardId,
|
||||
pendingFocusNoteId,
|
||||
multiDragDelta,
|
||||
shakeDirection,
|
||||
spawnOriginsRef,
|
||||
@@ -239,30 +233,6 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
|
||||
onBringToFront={onBringToFront}
|
||||
/>
|
||||
))}
|
||||
{Object.values(notes).map((n) => (
|
||||
<NoteCard
|
||||
key={`note-${n.note_id}`}
|
||||
noteId={n.note_id}
|
||||
cardX={n.x}
|
||||
cardY={n.y}
|
||||
cardWidth={n.width}
|
||||
cardHeight={n.height}
|
||||
cardZOrder={n.zOrder ?? 0}
|
||||
getCanvasState={getCanvasState}
|
||||
cmdHeld={cmdHeld}
|
||||
content={n.content}
|
||||
color={n.color}
|
||||
isSelected={selection.isSelected(n.note_id)}
|
||||
isHighlighted={highlightedCardId === n.note_id}
|
||||
multiDragDelta={multiDragDelta}
|
||||
autoFocus={pendingFocusNoteId === n.note_id}
|
||||
onCardSelect={onCardSelect}
|
||||
onDragStart={onDragStart}
|
||||
onDragMove={onDragMove}
|
||||
onDragEnd={onDragEnd}
|
||||
onBringToFront={onBringToFront}
|
||||
/>
|
||||
))}
|
||||
{workflowsHub && (
|
||||
<WorkflowsAppCard
|
||||
cardX={workflowsHub.x}
|
||||
|
||||
@@ -13,7 +13,7 @@ import DashboardGlyph from './DashboardGlyph';
|
||||
import ShareButton from '@/app/components/share/ShareButton';
|
||||
import type { AgentSession } from '@/shared/state/agentsSlice';
|
||||
import { saveLayout, viewCardKey } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition, NotePosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
import type { CanvasActions } from '../hooks/interaction/useCanvasControls';
|
||||
import { friendlyStatusLabel } from '@/shared/statusLabel';
|
||||
@@ -26,7 +26,6 @@ interface DashboardHeaderProps {
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
notes: Record<string, NotePosition>;
|
||||
expandedSessionIds: string[];
|
||||
outputs: Record<string, Output>;
|
||||
dashboardId: string | undefined;
|
||||
@@ -51,7 +50,6 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
||||
browserCards,
|
||||
workflowCards,
|
||||
workflowsHub,
|
||||
notes,
|
||||
expandedSessionIds,
|
||||
outputs,
|
||||
dashboardId,
|
||||
@@ -194,7 +192,7 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
||||
onOpen={() => {
|
||||
// Layout saves are debounced, so a just-added app/agent card may not be on disk yet. The export reads disk, flush the live layout now so Share captures the current board, not a stale one.
|
||||
if (!dashboardId) return;
|
||||
dispatch(saveLayout({ dashboardId, cards, viewCards, browserCards, workflowCards, workflowsHub, notes, expandedSessionIds }));
|
||||
dispatch(saveLayout({ dashboardId, cards, viewCards, browserCards, workflowCards, workflowsHub, expandedSessionIds }));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -49,7 +49,6 @@ interface DashboardOverlaysProps {
|
||||
onAddView: (outputId: string, opts?: { newInstance?: boolean }) => void;
|
||||
onHistoryResume: (sessionId: string) => void;
|
||||
onAddBrowser: () => void;
|
||||
onAddNote: () => void;
|
||||
onNewAgentBounceEnd: () => void;
|
||||
onFitToView: () => void;
|
||||
onTidy: () => void;
|
||||
@@ -84,7 +83,6 @@ const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
onAddView,
|
||||
onHistoryResume,
|
||||
onAddBrowser,
|
||||
onAddNote,
|
||||
onNewAgentBounceEnd,
|
||||
onFitToView,
|
||||
onTidy,
|
||||
@@ -108,7 +106,6 @@ const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
onAddView={onAddView}
|
||||
onHistoryResume={onHistoryResume}
|
||||
onAddBrowser={onAddBrowser}
|
||||
onAddNote={onAddNote}
|
||||
dashboardId={dashboardId}
|
||||
newAgentBounce={newAgentBounce}
|
||||
canvasEmpty={canvasEmpty}
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PaletteOutlinedIcon from '@mui/icons-material/PaletteOutlined';
|
||||
import {
|
||||
setNotePosition,
|
||||
setNoteSize,
|
||||
removeNote,
|
||||
updateNoteContent,
|
||||
setNoteColor,
|
||||
recordClosedCard,
|
||||
toggleMinimizeCard,
|
||||
setTiledCard,
|
||||
clearTiledCard,
|
||||
clearCardWindowState,
|
||||
NoteColor,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import WindowControls from './WindowControls';
|
||||
import { openCardContextMenu } from '../desktop/CardContextMenu';
|
||||
import { useTiledStyle } from './tileZones';
|
||||
import { useDragEndBackstops } from '../hooks/interaction/useDragEndBackstops';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
const EDGE_THICKNESS = 6;
|
||||
const CORNER_SIZE = 14;
|
||||
const MIN_W = 160;
|
||||
const MIN_H = 120;
|
||||
const HEADER_H = 18;
|
||||
|
||||
const CURSOR_MAP: Record<ResizeDir, string> = {
|
||||
n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize',
|
||||
nw: 'nwse-resize', se: 'nwse-resize', ne: 'nesw-resize', sw: 'nesw-resize',
|
||||
};
|
||||
|
||||
const HANDLE_DEFS: { dir: ResizeDir; sx: Record<string, any> }[] = [
|
||||
{ dir: 'n', sx: { top: -EDGE_THICKNESS / 2, left: CORNER_SIZE, right: CORNER_SIZE, height: EDGE_THICKNESS } },
|
||||
{ dir: 's', sx: { bottom: -EDGE_THICKNESS / 2, left: CORNER_SIZE, right: CORNER_SIZE, height: EDGE_THICKNESS } },
|
||||
{ dir: 'w', sx: { left: -EDGE_THICKNESS / 2, top: CORNER_SIZE, bottom: CORNER_SIZE, width: EDGE_THICKNESS } },
|
||||
{ dir: 'e', sx: { right: -EDGE_THICKNESS / 2, top: CORNER_SIZE, bottom: CORNER_SIZE, width: EDGE_THICKNESS } },
|
||||
{ dir: 'nw', sx: { top: -EDGE_THICKNESS / 2, left: -EDGE_THICKNESS / 2, width: CORNER_SIZE, height: CORNER_SIZE } },
|
||||
{ dir: 'ne', sx: { top: -EDGE_THICKNESS / 2, right: -EDGE_THICKNESS / 2, width: CORNER_SIZE, height: CORNER_SIZE } },
|
||||
{ dir: 'sw', sx: { bottom: -EDGE_THICKNESS / 2, left: -EDGE_THICKNESS / 2, width: CORNER_SIZE, height: CORNER_SIZE } },
|
||||
{ dir: 'se', sx: { bottom: -EDGE_THICKNESS / 2, right: -EDGE_THICKNESS / 2, width: CORNER_SIZE, height: CORNER_SIZE } },
|
||||
];
|
||||
|
||||
// Hand-tuned palette: distinct enough to skim, gentle in both themes.
|
||||
const NOTE_PALETTE: Record<NoteColor, { bg: string; border: string; text: string }> = {
|
||||
yellow: { bg: '#FBE89C', border: '#E0C95A', text: '#3a2e0a' },
|
||||
pink: { bg: '#F8C3D0', border: '#DB94A6', text: '#3a131e' },
|
||||
blue: { bg: '#B6D7F0', border: '#86B5D8', text: '#0e2a3d' },
|
||||
green: { bg: '#C7E5B5', border: '#94C376', text: '#1c3210' },
|
||||
purple: { bg: '#D8C5EE', border: '#A98BCB', text: '#23123e' },
|
||||
gray: { bg: '#DEDDD6', border: '#A8A6A0', text: '#262522' },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
cardX: number;
|
||||
cardY: number;
|
||||
cardWidth: number;
|
||||
cardHeight: number;
|
||||
getCanvasState: () => { panX: number; panY: number; zoom: number };
|
||||
cmdHeld?: boolean;
|
||||
isSelected?: boolean;
|
||||
isHighlighted?: boolean;
|
||||
multiDragDelta?: { dx: number; dy: number } | null;
|
||||
content: string;
|
||||
color: NoteColor;
|
||||
cardZOrder?: number;
|
||||
autoFocus?: boolean;
|
||||
onCardSelect?: (id: string, type: 'agent' | 'view' | 'browser' | 'note', shiftKey: boolean, originTarget?: EventTarget | null) => void;
|
||||
onDragStart?: (id: string, type: 'agent' | 'view' | 'browser' | 'note') => void;
|
||||
onDragMove?: (dx: number, dy: number, mouseX?: number, mouseY?: number) => void;
|
||||
onDragEnd?: (dx: number, dy: number, didDrag: boolean) => void;
|
||||
onBringToFront?: (id: string, type: 'agent' | 'view' | 'browser' | 'note') => void;
|
||||
}
|
||||
|
||||
const NoteCard: React.FC<Props> = ({
|
||||
noteId, cardX, cardY, cardWidth, cardHeight, getCanvasState,
|
||||
isSelected = false, isHighlighted = false, multiDragDelta, content, color,
|
||||
cardZOrder = 0, autoFocus, onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
|
||||
}) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const palette = NOTE_PALETTE[color] || NOTE_PALETTE.yellow;
|
||||
const isMinimized = useAppSelector((s) => !!s.dashboardLayout.minimizedCards[noteId]);
|
||||
const tileZone = useAppSelector((s) => s.dashboardLayout.tiledCards[noteId]);
|
||||
|
||||
const DRAG_THRESHOLD = 3;
|
||||
const dragState = useRef<{ startX: number; startY: number; origX: number; origY: number; startPanX: number; startPanY: number } | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [localDragPos, setLocalDragPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const didDrag = useRef(false);
|
||||
const justDraggedRef = useRef(false);
|
||||
const lastPointerRef = useRef<{ clientX: number; clientY: number }>({ clientX: 0, clientY: 0 });
|
||||
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus && textareaRef.current) {
|
||||
// Defer so the card has mounted in its final position.
|
||||
const t = setTimeout(() => textareaRef.current?.focus(), 50);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
const handleDragPointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
if (tileZone) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const cs = getCanvasState();
|
||||
dragState.current = {
|
||||
startX: e.clientX, startY: e.clientY,
|
||||
origX: cardX, origY: cardY,
|
||||
startPanX: cs.panX, startPanY: cs.panY,
|
||||
};
|
||||
lastPointerRef.current = { clientX: e.clientX, clientY: e.clientY };
|
||||
didDrag.current = false;
|
||||
setIsDragging(true);
|
||||
try { (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); } catch { /* pointer already gone */ }
|
||||
onDragStart?.(noteId, 'note');
|
||||
}, [cardX, cardY, noteId, onDragStart, getCanvasState, tileZone]);
|
||||
|
||||
const recomputeDragPos = useCallback(() => {
|
||||
const ds = dragState.current;
|
||||
if (!ds || !didDrag.current) return;
|
||||
const { clientX, clientY } = lastPointerRef.current;
|
||||
const rawDx = clientX - ds.startX;
|
||||
const rawDy = clientY - ds.startY;
|
||||
const cs = getCanvasState();
|
||||
const z = cs.zoom;
|
||||
const panDx = (cs.panX - ds.startPanX) / z;
|
||||
const panDy = (cs.panY - ds.startPanY) / z;
|
||||
const dx = rawDx / z - panDx;
|
||||
const dy = rawDy / z - panDy;
|
||||
setLocalDragPos({ x: ds.origX + dx, y: ds.origY + dy });
|
||||
onDragMove?.(dx, dy, clientX, clientY);
|
||||
}, [onDragMove, getCanvasState]);
|
||||
|
||||
// Edge-pan/wheel-zoom moves the camera without a React commit; the pan-changed event is the live signal to re-pin the card to the cursor.
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
const onPanChange = () => {
|
||||
if (didDrag.current) recomputeDragPos();
|
||||
};
|
||||
window.addEventListener('openswarm:canvas-pan-changed', onPanChange);
|
||||
return () => window.removeEventListener('openswarm:canvas-pan-changed', onPanChange);
|
||||
}, [isDragging, recomputeDragPos]);
|
||||
|
||||
const handleDragPointerMove = useCallback((e: React.PointerEvent) => {
|
||||
if (!dragState.current) return;
|
||||
const rawDx = e.clientX - dragState.current.startX;
|
||||
const rawDy = e.clientY - dragState.current.startY;
|
||||
if (!didDrag.current && Math.sqrt(rawDx * rawDx + rawDy * rawDy) < DRAG_THRESHOLD) return;
|
||||
didDrag.current = true;
|
||||
lastPointerRef.current = { clientX: e.clientX, clientY: e.clientY };
|
||||
recomputeDragPos();
|
||||
}, [recomputeDragPos]);
|
||||
|
||||
const finalizeDrag = useCallback((clientX: number, clientY: number, shiftKey: boolean) => {
|
||||
if (!dragState.current) return;
|
||||
const cs = getCanvasState();
|
||||
const z = cs.zoom;
|
||||
const panDx = (cs.panX - dragState.current.startPanX) / z;
|
||||
const panDy = (cs.panY - dragState.current.startPanY) / z;
|
||||
const dx = (clientX - dragState.current.startX) / z - panDx;
|
||||
const dy = (clientY - dragState.current.startY) / z - panDy;
|
||||
if (didDrag.current) {
|
||||
let finalX = dragState.current.origX + dx;
|
||||
let finalY = dragState.current.origY + dy;
|
||||
if (!shiftKey) {
|
||||
finalX = Math.round(finalX / 24) * 24;
|
||||
finalY = Math.round(finalY / 24) * 24;
|
||||
}
|
||||
dispatch(setNotePosition({ noteId, x: finalX, y: finalY }));
|
||||
justDraggedRef.current = true;
|
||||
requestAnimationFrame(() => { justDraggedRef.current = false; });
|
||||
}
|
||||
onDragEnd?.(dx, dy, didDrag.current);
|
||||
dragState.current = null;
|
||||
didDrag.current = false;
|
||||
setLocalDragPos(null);
|
||||
setIsDragging(false);
|
||||
}, [dispatch, noteId, onDragEnd, getCanvasState]);
|
||||
|
||||
const handleDragPointerUp = useCallback((e: React.PointerEvent) => {
|
||||
if (!dragState.current) return;
|
||||
finalizeDrag(e.clientX, e.clientY, e.shiftKey);
|
||||
try { (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); } catch { /* capture already gone */ }
|
||||
}, [finalizeDrag]);
|
||||
|
||||
const abortDrag = useCallback(() => {
|
||||
if (!dragState.current) return;
|
||||
finalizeDrag(lastPointerRef.current.clientX, lastPointerRef.current.clientY, true);
|
||||
}, [finalizeDrag]);
|
||||
useDragEndBackstops(isDragging, finalizeDrag, abortDrag);
|
||||
|
||||
const resizeRef = useRef<{
|
||||
dir: ResizeDir; startX: number; startY: number;
|
||||
origX: number; origY: number; origW: number; origH: number;
|
||||
} | null>(null);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [localResize, setLocalResize] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
|
||||
|
||||
const handleResizeDown = useCallback(
|
||||
(dir: ResizeDir) => (e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizeRef.current = {
|
||||
dir, startX: e.clientX, startY: e.clientY,
|
||||
origX: cardX, origY: cardY, origW: cardWidth, origH: cardHeight,
|
||||
};
|
||||
setIsResizing(true);
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
},
|
||||
[cardX, cardY, cardWidth, cardHeight],
|
||||
);
|
||||
|
||||
const computeResize = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!resizeRef.current) return null;
|
||||
const { dir, startX, startY, origX, origY, origW, origH } = resizeRef.current;
|
||||
const zoom = getCanvasState().zoom;
|
||||
const dx = (e.clientX - startX) / zoom;
|
||||
const dy = (e.clientY - startY) / zoom;
|
||||
let newX = origX, newY = origY, newW = origW, newH = origH;
|
||||
if (dir.includes('e')) newW = origW + dx;
|
||||
if (dir.includes('w')) { newW = origW - dx; newX = origX + dx; }
|
||||
if (dir.includes('s')) newH = origH + dy;
|
||||
if (dir.includes('n')) { newH = origH - dy; newY = origY + dy; }
|
||||
if (newW < MIN_W) { if (dir.includes('w')) newX = origX + origW - MIN_W; newW = MIN_W; }
|
||||
if (newH < MIN_H) { if (dir.includes('n')) newY = origY + origH - MIN_H; newH = MIN_H; }
|
||||
return { x: newX, y: newY, w: newW, h: newH };
|
||||
},
|
||||
[getCanvasState],
|
||||
);
|
||||
|
||||
const handleResizeMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
const result = computeResize(e);
|
||||
if (result) setLocalResize(result);
|
||||
},
|
||||
[computeResize],
|
||||
);
|
||||
|
||||
const handleResizeUp = useCallback((e: React.PointerEvent) => {
|
||||
if (!resizeRef.current) return;
|
||||
const result = computeResize(e);
|
||||
if (result) {
|
||||
dispatch(setNotePosition({ noteId, x: result.x, y: result.y }));
|
||||
dispatch(setNoteSize({ noteId, width: result.w, height: result.h }));
|
||||
}
|
||||
resizeRef.current = null;
|
||||
setLocalResize(null);
|
||||
setIsResizing(false);
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
}, [computeResize, dispatch, noteId]);
|
||||
|
||||
const handleRemove = (e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
dispatch(clearCardWindowState(noteId));
|
||||
dispatch(recordClosedCard({ kind: 'note', id: noteId }));
|
||||
dispatch(removeNote(noteId));
|
||||
};
|
||||
const onMinimize = () => dispatch(toggleMinimizeCard({ cardId: noteId }));
|
||||
const onTile = (zone: string) => {
|
||||
if (zone === 'restore') dispatch(clearTiledCard(noteId));
|
||||
else dispatch(setTiledCard({ cardId: noteId, zone }));
|
||||
};
|
||||
// Tiled geometry must track pan/zoom, but the camera lives outside React now; subscribe to the pan event ONLY while tiled and read the live getter.
|
||||
const [tileTick, setTileTick] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!tileZone) return undefined;
|
||||
const onPan = (): void => setTileTick((t) => t + 1);
|
||||
window.addEventListener('openswarm:canvas-pan-changed', onPan);
|
||||
return () => window.removeEventListener('openswarm:canvas-pan-changed', onPan);
|
||||
}, [tileZone]);
|
||||
void tileTick;
|
||||
const cam = getCanvasState();
|
||||
const tiledStyle = useTiledStyle(tileZone, cam.panX, cam.panY, cam.zoom, getCanvasState, noteId);
|
||||
const isFullscreen = tileZone === 'fullscreen';
|
||||
|
||||
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
|
||||
const mdDy = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dy : 0;
|
||||
const displayX = localResize?.x ?? localDragPos?.x ?? (cardX + mdDx);
|
||||
const displayY = localResize?.y ?? localDragPos?.y ?? (cardY + mdDy);
|
||||
const displayW = localResize?.w ?? cardWidth;
|
||||
const displayH = localResize?.h ?? cardHeight;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="osw-card"
|
||||
data-select-type="note-card"
|
||||
data-select-id={noteId}
|
||||
onContextMenu={(e: React.MouseEvent) => openCardContextMenu(e, {
|
||||
items: [
|
||||
{ label: 'Full Screen', onClick: () => onTile('fullscreen') },
|
||||
{ label: 'Minimize', onClick: onMinimize },
|
||||
{ label: 'Delete note', danger: true, onClick: () => handleRemove() },
|
||||
],
|
||||
})}
|
||||
data-select-meta={JSON.stringify({ name: 'Note', content: content.slice(0, 60) })}
|
||||
onPointerDownCapture={(e: React.PointerEvent) => {
|
||||
onBringToFront?.(noteId, 'note');
|
||||
// Capture-phase so a click the textarea swallows still selects the note; shift keeps the bubbled toggle path. Pass the target so a textarea press selects without yanking the camera.
|
||||
if (e.button === 0 && !e.shiftKey) onCardSelect?.(noteId, 'note', false, e.target);
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
if (justDraggedRef.current) return;
|
||||
onCardSelect?.(noteId, 'note', e.shiftKey);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: tiledStyle ? tiledStyle.left : displayX,
|
||||
top: tiledStyle ? tiledStyle.top : displayY,
|
||||
width: tiledStyle ? tiledStyle.width : (isMinimized ? 190 : displayW),
|
||||
height: tiledStyle ? tiledStyle.height : (isMinimized ? 32 : displayH),
|
||||
transform: tiledStyle ? tiledStyle.transform : undefined,
|
||||
transformOrigin: tiledStyle ? tiledStyle.transformOrigin : undefined,
|
||||
// contain + willChange: own compositor layer so paint stays scoped (see AgentCard for full rationale).
|
||||
contain: 'layout style',
|
||||
willChange: 'transform',
|
||||
borderRadius: isFullscreen ? '12px' : `${c.radius.md}px`,
|
||||
bgcolor: palette.bg,
|
||||
border: isHighlighted
|
||||
? `2px solid ${c.accent.primary}`
|
||||
: isSelected ? '2px solid #3b82f6' : `1px solid ${palette.border}`,
|
||||
boxShadow: isHighlighted
|
||||
? `0 0 0 3px ${c.accent.primary}50, 0 0 20px ${c.accent.primary}35`
|
||||
: isDragging || isResizing
|
||||
? c.shadow.lg
|
||||
: isSelected
|
||||
? `0 0 0 1px #3b82f6, ${c.shadow.md}`
|
||||
: c.shadow.sm,
|
||||
zIndex: tiledStyle ? 999990 : (isDragging || isResizing) ? 999999 : cardZOrder,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'&:hover .note-controls': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
{/* Drag header */}
|
||||
<Box
|
||||
onPointerDown={handleDragPointerDown}
|
||||
onPointerMove={handleDragPointerMove}
|
||||
onPointerUp={handleDragPointerUp}
|
||||
onPointerCancel={abortDrag}
|
||||
onLostPointerCapture={abortDrag}
|
||||
sx={{
|
||||
height: isMinimized ? '100%' : HEADER_H,
|
||||
flexShrink: 0,
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 0.75,
|
||||
touchAction: 'none',
|
||||
}}
|
||||
>
|
||||
<Box onPointerDown={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} noTileMenu={tileZone === 'fullscreen'} />
|
||||
</Box>
|
||||
{isMinimized && (
|
||||
<Box sx={{ flex: 1, minWidth: 0, fontSize: '0.8125rem', color: palette.text, opacity: 0.75, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{content.trim() || 'Note'}
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
className="note-controls"
|
||||
sx={{ ml: 'auto', opacity: 0, transition: 'opacity 0.15s', display: isMinimized ? 'none' : 'flex' }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); setShowColorPicker((v) => !v); }}
|
||||
sx={{ p: 0.25, color: palette.text, opacity: 0.55, '&:hover': { opacity: 1, bgcolor: 'rgba(0,0,0,0.06)' } }}
|
||||
>
|
||||
<PaletteOutlinedIcon sx={{ fontSize: 13 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{showColorPicker && (
|
||||
<Box
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: HEADER_H + 2,
|
||||
left: 8,
|
||||
display: 'flex',
|
||||
gap: 0.5,
|
||||
p: 0.75,
|
||||
bgcolor: 'rgba(255,255,255,0.95)',
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
borderRadius: `${c.radius.sm}px`,
|
||||
boxShadow: c.shadow.md,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
{(Object.keys(NOTE_PALETTE) as NoteColor[]).map((key) => {
|
||||
const p = NOTE_PALETTE[key];
|
||||
const active = key === color;
|
||||
return (
|
||||
<Box
|
||||
key={key}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setNoteColor({ noteId, color: key }));
|
||||
setShowColorPicker(false);
|
||||
}}
|
||||
sx={{
|
||||
width: 16, height: 16, borderRadius: '50%',
|
||||
bgcolor: p.bg,
|
||||
border: active ? `2px solid #3b82f6` : `1px solid ${p.border}`,
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.1s',
|
||||
'&:hover': { transform: 'scale(1.15)' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Editable content. Fullscreen = focus-writing mode: reading-size type in a centered column, like Bear/Arc, not 12px lost in a 2800px card. */}
|
||||
{!isMinimized && (
|
||||
<Box sx={{ flex: 1, p: 1, pt: 0.25, display: 'flex', justifyContent: 'center', minHeight: 0 }}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => dispatch(updateNoteContent({ noteId, content: e.target.value }))}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
placeholder="Type a note…"
|
||||
spellCheck
|
||||
style={{
|
||||
flex: 1,
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
background: 'transparent',
|
||||
color: palette.text,
|
||||
fontFamily: c.font.sans,
|
||||
fontSize: isFullscreen ? 'clamp(1.1rem, 1.3vw, 1.5rem)' : '0.85rem',
|
||||
lineHeight: isFullscreen ? 1.6 : 1.45,
|
||||
padding: isFullscreen ? '4vh 0 0' : 0,
|
||||
maxWidth: isFullscreen ? 'min(72ch, 82%)' : undefined,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Resize handles */}
|
||||
{!isMinimized && HANDLE_DEFS.map(({ dir, sx }) => (
|
||||
<Box
|
||||
key={dir}
|
||||
onPointerDown={handleResizeDown(dir)}
|
||||
onPointerMove={handleResizeMove}
|
||||
onPointerUp={handleResizeUp}
|
||||
onPointerCancel={handleResizeUp}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
cursor: CURSOR_MAP[dir],
|
||||
zIndex: 5,
|
||||
touchAction: 'none',
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NoteCard);
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// One right-click menu for every canvas entity (chats, browsers, notes, apps, workflow cards,
|
||||
// One right-click menu for every canvas entity (chats, browsers, apps, workflow cards,
|
||||
// minimized pills). Cards call openCardContextMenu with their items; this overlay renders the
|
||||
// native-feeling glass menu (SpacesStrip grammar) and closes on outside press / Esc / item click.
|
||||
export interface CardMenuItem {
|
||||
|
||||
@@ -4,7 +4,6 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined';
|
||||
import { openWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import AppsRoundedIcon from '@mui/icons-material/AppsRounded';
|
||||
@@ -17,7 +16,6 @@ import type {
|
||||
CardPosition,
|
||||
ViewCardPosition,
|
||||
BrowserCardPosition,
|
||||
NotePosition,
|
||||
WorkflowCardPosition,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
@@ -27,14 +25,12 @@ interface DesktopDockProps {
|
||||
cards: Record<string, CardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
notes: Record<string, NotePosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
outputs: Record<string, Output>;
|
||||
selectedIds: string[];
|
||||
onFocusCard: (id: string, rect: CardRect) => void;
|
||||
onApplications: () => void;
|
||||
onAddBrowser: () => void;
|
||||
onAddNote: () => void;
|
||||
}
|
||||
|
||||
const TILE = 30;
|
||||
@@ -46,14 +42,12 @@ function DesktopDock({
|
||||
cards,
|
||||
viewCards,
|
||||
browserCards,
|
||||
notes,
|
||||
workflowCards,
|
||||
outputs,
|
||||
selectedIds,
|
||||
onFocusCard,
|
||||
onApplications,
|
||||
onAddBrowser,
|
||||
onAddNote,
|
||||
}: DesktopDockProps): React.ReactElement | null {
|
||||
const dispatch = useAppDispatch();
|
||||
const dockBodyRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -92,8 +86,8 @@ function DesktopDock({
|
||||
const hoverTimer = useRef<number | null>(null);
|
||||
|
||||
const entries = useMemo<DockEntry[]>(
|
||||
() => buildDockEntries({ sessions, cards, viewCards, browserCards, notes, workflowCards, outputs }),
|
||||
[sessions, cards, viewCards, browserCards, notes, workflowCards, outputs],
|
||||
() => buildDockEntries({ sessions, cards, viewCards, browserCards, workflowCards, outputs }),
|
||||
[sessions, cards, viewCards, browserCards, workflowCards, outputs],
|
||||
);
|
||||
|
||||
const beginHover = useCallback(
|
||||
@@ -201,11 +195,10 @@ function DesktopDock({
|
||||
{entries.length > 0 && (
|
||||
<Box sx={{ width: TILE - 8, height: '1px', background: 'rgba(255,255,255,0.14)' }} />
|
||||
)}
|
||||
{/* The og toolbar's actions, dock-resident: browser, workflow, note, then settings + apps below their own divider. New-chat lives in the spawn pill, history on the top island. */}
|
||||
{/* The og toolbar's actions, dock-resident: browser, workflow, then settings + apps below their own divider. New-chat lives in the spawn pill, history on the top island. */}
|
||||
{([
|
||||
{ label: 'New browser', icon: <LanguageIcon sx={{ fontSize: 17, color: '#e8e8ee' }} />, act: onAddBrowser },
|
||||
{ label: 'Workflows', icon: <EventRepeatIcon sx={{ fontSize: 16, color: '#e8e8ee' }} />, act: () => dispatch(openWorkflowsApp()) },
|
||||
{ label: 'New note', icon: <StickyNote2OutlinedIcon sx={{ fontSize: 16, color: '#e8e8ee' }} />, act: onAddNote },
|
||||
{ label: 'Settings', icon: <SettingsIcon sx={{ fontSize: 18, color: '#e8e8ee' }} />, act: () => dispatch(openSettingsModal(undefined)), divider: true },
|
||||
{ label: 'Applications', icon: <AppsRoundedIcon sx={{ fontSize: 18, color: '#e8e8ee' }} />, act: onApplications, bg: 'linear-gradient(135deg, #3d3d46, #232329)' },
|
||||
] as { label: string; icon: React.ReactNode; act: () => void; divider?: boolean; bg?: string }[]).map((a) => (
|
||||
|
||||
@@ -10,14 +10,12 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useVoice } from '@/shared/voice/voiceContext';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded';
|
||||
import StickyNote2OutlinedIcon from '@mui/icons-material/StickyNote2Outlined';
|
||||
import HistoryRoundedIcon from '@mui/icons-material/HistoryRounded';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
|
||||
interface DesktopSpawnPillProps {
|
||||
onOpenComposer: () => void;
|
||||
onAddNote: () => void;
|
||||
onAddBrowser: () => void;
|
||||
onAddApp: () => void;
|
||||
onWorkflows: () => void;
|
||||
@@ -25,7 +23,6 @@ interface DesktopSpawnPillProps {
|
||||
}
|
||||
|
||||
const MENU_ITEMS: Array<{ key: string; label: string; icon: React.ElementType }> = [
|
||||
{ key: 'note', label: 'Add note', icon: StickyNote2OutlinedIcon },
|
||||
{ key: 'browser', label: 'Browser', icon: LanguageIcon },
|
||||
{ key: 'app', label: 'Add app', icon: GridViewRoundedIcon },
|
||||
{ key: 'workflows', label: 'Workflows', icon: EventRepeatIcon },
|
||||
@@ -35,7 +32,6 @@ const MENU_ITEMS: Array<{ key: string; label: string; icon: React.ElementType }>
|
||||
/** Collapsed desktop composer: one dark pill that spawns an agent; + tucks the add actions away. */
|
||||
function DesktopSpawnPill({
|
||||
onOpenComposer,
|
||||
onAddNote,
|
||||
onAddBrowser,
|
||||
onAddApp,
|
||||
onWorkflows,
|
||||
@@ -60,7 +56,6 @@ function DesktopSpawnPill({
|
||||
}, [menuOpen]);
|
||||
|
||||
const actions: Record<string, () => void> = {
|
||||
note: onAddNote,
|
||||
browser: onAddBrowser,
|
||||
app: onAddApp,
|
||||
workflows: onWorkflows,
|
||||
|
||||
@@ -26,8 +26,8 @@ const HELP_SYSTEM_PROMPT = [
|
||||
'Answer questions about using OpenSwarm clearly and briefly, with the exact clicks or keys.',
|
||||
'What you know about the app:',
|
||||
'- Dashboards work like macOS Spaces: resting the cursor on the very top edge of the window reveals the spaces bar to switch dashboards or add one with +.',
|
||||
'- The canvas holds cards: agent chats, browsers, notes, built apps, and workflows. Cards have mac-style traffic lights; the green dot goes full screen, hovering it offers halves, quarters, and thirds.',
|
||||
'- The dark dock on the left creates chats, browsers, workflows, and notes, and opens History, Settings, and Apps.',
|
||||
'- The canvas holds cards: agent chats, browsers, built apps, and workflows. Cards have mac-style traffic lights; the green dot goes full screen, hovering it offers halves, quarters, and thirds.',
|
||||
'- The dark dock on the left creates chats, browsers, and workflows, and opens History, Settings, and Apps.',
|
||||
'- Cmd+K searches everything. Dictation: hold the mic in the Help pill (or the mic key) to talk; the words land where the cursor is.',
|
||||
'- Settings (gear in the dock): Account, General (agent defaults), Appearance (theme, accent colors, text size), Privacy, Advanced, plus Models (connect Claude, ChatGPT, or Gemini subscriptions or API keys), Skills, Tools, Commands, and Usage.',
|
||||
'- Workflows run agents on a schedule; open them from the dock calendar icon.',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
|
||||
import { MessageCircle } from 'lucide-react';
|
||||
import { pickIcon } from '../canvas/DashboardGlyph';
|
||||
@@ -11,7 +10,6 @@ import type {
|
||||
CardPosition,
|
||||
ViewCardPosition,
|
||||
BrowserCardPosition,
|
||||
NotePosition,
|
||||
WorkflowCardPosition,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
@@ -40,7 +38,6 @@ export interface DockSlices {
|
||||
cards: Record<string, CardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
notes: Record<string, NotePosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
outputs: Record<string, Output>;
|
||||
}
|
||||
@@ -60,7 +57,7 @@ function hueFor(name: string): string {
|
||||
return AGENT_TILE_HUES[Math.abs(h) % AGENT_TILE_HUES.length];
|
||||
}
|
||||
|
||||
export function buildDockEntries({ sessions, cards, viewCards, browserCards, notes, workflowCards, outputs }: DockSlices): DockEntry[] {
|
||||
export function buildDockEntries({ sessions, cards, viewCards, browserCards, workflowCards, outputs }: DockSlices): DockEntry[] {
|
||||
const list: DockEntry[] = [];
|
||||
for (const card of Object.values(cards)) {
|
||||
const session = sessions[card.session_id];
|
||||
@@ -101,17 +98,6 @@ export function buildDockEntries({ sessions, cards, viewCards, browserCards, not
|
||||
thumbnail: output?.thumbnail,
|
||||
});
|
||||
}
|
||||
for (const note of Object.values(notes)) {
|
||||
const firstLine = (note.content || '').split('\n')[0].trim();
|
||||
list.push({
|
||||
id: note.note_id,
|
||||
label: firstLine || 'Note',
|
||||
rect: note,
|
||||
tileBg: 'linear-gradient(135deg, #f2d270, #e0b23e)',
|
||||
icon: <EditNoteIcon sx={{ fontSize: 18, color: '#7a5d10' }} />,
|
||||
snippet: (note.content || '').slice(0, 140),
|
||||
});
|
||||
}
|
||||
for (const [cardKey, wf] of Object.entries(workflowCards)) {
|
||||
list.push({
|
||||
id: cardKey,
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ContentBounds {
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
// Bounding box over agent + view + browser cards (notes intentionally excluded, same as before). Returns undefined for an empty canvas.
|
||||
// Bounding box over agent + view + browser cards. Returns undefined for an empty canvas.
|
||||
export function computeContentBounds(
|
||||
cards: Record<string, CardPosition>,
|
||||
viewCards: Record<string, ViewCardPosition>,
|
||||
|
||||
@@ -17,10 +17,6 @@ export function getCardRect(id: string, type: CardType):
|
||||
const bc = layoutState.browserCards[id];
|
||||
if (!bc) return undefined;
|
||||
return { x: bc.x, y: bc.y, width: bc.width, height: bc.height };
|
||||
} else if (type === 'note') {
|
||||
const n = layoutState.notes[id];
|
||||
if (!n) return undefined;
|
||||
return { x: n.x, y: n.y, width: n.width, height: n.height };
|
||||
} else if (type === 'workflow') {
|
||||
const wc = layoutState.workflowCards[id];
|
||||
if (!wc) return undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { closeSession } from '@/shared/state/agentsSlice';
|
||||
import { removeNote, removeWorkflowCard, closeWorkflowsHub, recordClosedCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { removeWorkflowCard, closeWorkflowsHub, recordClosedCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { closeWorkflowCard } from '@/shared/state/workflowsSlice';
|
||||
import { removeBrowserCardsCleanly } from '@/shared/browserTeardown';
|
||||
import { removeViewCardCleanly } from '@/shared/viewTeardown';
|
||||
@@ -20,9 +20,6 @@ export function deleteSelectedCards(selectedIds: Map<string, CardType>, dispatch
|
||||
} else if (type === 'browser') {
|
||||
dispatch(recordClosedCard({ kind: 'browser', id }));
|
||||
browserIds.push(id);
|
||||
} else if (type === 'note') {
|
||||
dispatch(recordClosedCard({ kind: 'note', id }));
|
||||
dispatch(removeNote(id));
|
||||
} else if (type === 'workflow') {
|
||||
dispatch(recordClosedCard({ kind: 'workflow', id }));
|
||||
dispatch(removeWorkflowCard(id));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, type Dispatch, type RefObject, type SetStateAction } from 'react';
|
||||
import { useCallback, type Dispatch, type RefObject, type SetStateAction } from 'react';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
import { store } from '@/shared/state/store';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
@@ -7,14 +7,10 @@ import {
|
||||
tidyLayout,
|
||||
addViewCard,
|
||||
addBrowserCard,
|
||||
addNote,
|
||||
clearPendingFocusNoteId,
|
||||
DEFAULT_VIEW_CARD_W,
|
||||
DEFAULT_VIEW_CARD_H,
|
||||
DEFAULT_BROWSER_CARD_W,
|
||||
DEFAULT_BROWSER_CARD_H,
|
||||
DEFAULT_NOTE_W,
|
||||
DEFAULT_NOTE_H,
|
||||
EXPANDED_CARD_MIN_H,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
|
||||
@@ -26,7 +22,6 @@ type Selection = ReturnType<typeof useDashboardSelection>;
|
||||
interface UseDashboardCardActionsArgs {
|
||||
expandedSessionIds: string[];
|
||||
browserHomepage: string;
|
||||
pendingFocusNoteId: string | null;
|
||||
selection: Selection;
|
||||
canvasActions: CanvasActions;
|
||||
getCardRect: (id: string, type: CardType) => { x: number; y: number; width: number; height: number } | undefined;
|
||||
@@ -39,7 +34,6 @@ interface UseDashboardCardActionsArgs {
|
||||
export function useDashboardCardActions({
|
||||
expandedSessionIds,
|
||||
browserHomepage,
|
||||
pendingFocusNoteId,
|
||||
selection,
|
||||
canvasActions,
|
||||
getCardRect,
|
||||
@@ -78,29 +72,6 @@ export function useDashboardCardActions({
|
||||
dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds, x: pos.x, y: pos.y }));
|
||||
}, [dispatch, browserHomepage, expandedSessionIds, getSpawnPlacement]);
|
||||
|
||||
const handleAddNote = useCallback(() => {
|
||||
report('dashboard', 'note_added');
|
||||
const prevIds = new Set(Object.keys(store.getState().dashboardLayout.notes));
|
||||
const pos = getSpawnPlacement(DEFAULT_NOTE_W, DEFAULT_NOTE_H);
|
||||
dispatch(addNote({ expandedSessionIds, x: pos.x, y: pos.y }));
|
||||
setTimeout(() => {
|
||||
const allNotes = store.getState().dashboardLayout.notes;
|
||||
const newId = Object.keys(allNotes).find((id) => !prevIds.has(id));
|
||||
if (newId) {
|
||||
const note = allNotes[newId];
|
||||
canvasActions.revealCards([{ x: note.x, y: note.y, width: note.width, height: note.height }]);
|
||||
handleHighlightCard(newId);
|
||||
}
|
||||
}, 200);
|
||||
}, [dispatch, expandedSessionIds, getSpawnPlacement, canvasActions, handleHighlightCard]);
|
||||
|
||||
// Auto-clear pendingFocusNoteId after the note has had a chance to mount + autofocus.
|
||||
useEffect(() => {
|
||||
if (!pendingFocusNoteId) return;
|
||||
const t = setTimeout(() => dispatch(clearPendingFocusNoteId()), 800);
|
||||
return () => clearTimeout(t);
|
||||
}, [pendingFocusNoteId, dispatch]);
|
||||
|
||||
const handleHistoryResume = useCallback((sessionId: string) => {
|
||||
dispatch(resumeSession({ sessionId })).then((action) => {
|
||||
if (resumeSession.fulfilled.match(action)) {
|
||||
@@ -157,7 +128,6 @@ export function useDashboardCardActions({
|
||||
return {
|
||||
handleAddView,
|
||||
handleAddBrowser,
|
||||
handleAddNote,
|
||||
handleHistoryResume,
|
||||
handleFitToView,
|
||||
handleTidy,
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
dashboardName, sessions, expandedSessionIds, cards, viewCards, browserCards, keepAliveBrowserCards,
|
||||
workflowCards, workflowItems, workflowOpenCards, workflowsHub,
|
||||
pendingFocusWorkflowId, pendingFocusWorkflowsHub,
|
||||
notes, pendingFocusNoteId, layoutInitialized, persistedExpandedSessionIds,
|
||||
layoutInitialized, persistedExpandedSessionIds,
|
||||
zoomSensitivity, newAgentShortcut, browserHomepage, expandNewChats,
|
||||
autoRevealSubAgents, outputs, outputsLoaded, glowingAgentCards, glowingBrowserCards,
|
||||
} = useDashboardSelectors(dashboardId);
|
||||
@@ -74,7 +74,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
cards,
|
||||
viewCards,
|
||||
browserCards,
|
||||
notes,
|
||||
workflowCards,
|
||||
workflowsHub,
|
||||
);
|
||||
@@ -200,7 +199,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
browserCards,
|
||||
workflowCards,
|
||||
workflowsHub,
|
||||
notes,
|
||||
expandedSessionIds,
|
||||
captureNow,
|
||||
});
|
||||
@@ -281,14 +279,12 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
const {
|
||||
handleAddView,
|
||||
handleAddBrowser,
|
||||
handleAddNote,
|
||||
handleHistoryResume,
|
||||
handleFitToView,
|
||||
handleTidy,
|
||||
} = useDashboardCardActions({
|
||||
expandedSessionIds,
|
||||
browserHomepage,
|
||||
pendingFocusNoteId,
|
||||
selection,
|
||||
canvasActions: canvas.actions,
|
||||
getCardRect,
|
||||
@@ -332,10 +328,10 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
|
||||
return {
|
||||
c, dashboardId, dashboardName, canvas, selection, sessions, sessionList,
|
||||
cards, viewCards, browserCards, keepAliveBrowserCards, notes, outputs, glowingAgentCards,
|
||||
cards, viewCards, browserCards, keepAliveBrowserCards, outputs, glowingAgentCards,
|
||||
workflowCards, workflowsHub,
|
||||
expandedSessionIds, tethers, highlightedCardId, autoFocusSessionId,
|
||||
focusedCardId, pendingFocusNoteId, multiDragDelta, shakeDirection,
|
||||
focusedCardId, multiDragDelta, shakeDirection,
|
||||
neighborDirections, toolbarOpen, searchPaletteOpen, newAgentBounce, canvasEmpty,
|
||||
toolbarRef, spawnOriginsRef, revealSpawnedRef, measuredHeightsRef, getCanvasState,
|
||||
toolbarPrefill,
|
||||
@@ -360,7 +356,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
onAddView: handleAddView,
|
||||
onHistoryResume: handleHistoryResume,
|
||||
onAddBrowser: handleAddBrowser,
|
||||
onAddNote: handleAddNote,
|
||||
onNewAgentBounceEnd: () => {
|
||||
bounceDismissedRef.current = true;
|
||||
setNewAgentBounce(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect, RefObject } from 'react';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition, NotePosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { viewCardKey } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
export type { CardType } from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -43,7 +43,6 @@ export function useDashboardSelection(
|
||||
cards: Record<string, CardPosition>,
|
||||
viewCards: Record<string, ViewCardPosition>,
|
||||
browserCards: Record<string, BrowserCardPosition> = {},
|
||||
notes: Record<string, NotePosition> = {},
|
||||
workflowCards: Record<string, WorkflowCardPosition> = {},
|
||||
workflowsHub: WorkflowsHubPosition | null = null,
|
||||
) {
|
||||
@@ -78,11 +77,10 @@ export function useDashboardSelection(
|
||||
for (const card of Object.values(cards)) next.set(card.session_id, 'agent');
|
||||
for (const vc of Object.values(viewCards)) next.set(viewCardKey(vc.output_id, vc.instance), 'view');
|
||||
for (const bc of Object.values(browserCards)) next.set(bc.browser_id, 'browser');
|
||||
for (const n of Object.values(notes)) next.set(n.note_id, 'note');
|
||||
for (const wc of Object.values(workflowCards)) next.set(wc.workflow_id, 'workflow');
|
||||
if (workflowsHub) next.set('workflows-hub', 'workflows-hub');
|
||||
setSelectedIds(next);
|
||||
}, [cards, viewCards, browserCards, notes, workflowCards, workflowsHub]);
|
||||
}, [cards, viewCards, browserCards, workflowCards, workflowsHub]);
|
||||
|
||||
const selectCard = useCallback(
|
||||
(id: string, type: CardType, shiftKey: boolean) => {
|
||||
@@ -157,19 +155,6 @@ export function useDashboardSelection(
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of Object.values(notes)) {
|
||||
if (
|
||||
rectsIntersect(rect, {
|
||||
x: n.x,
|
||||
y: n.y,
|
||||
width: n.width,
|
||||
height: n.height,
|
||||
})
|
||||
) {
|
||||
intersecting.set(n.note_id, 'note');
|
||||
}
|
||||
}
|
||||
|
||||
for (const wc of Object.values(workflowCards)) {
|
||||
if (
|
||||
rectsIntersect(rect, {
|
||||
@@ -210,7 +195,7 @@ export function useDashboardSelection(
|
||||
|
||||
return intersecting;
|
||||
},
|
||||
[cards, viewCards, browserCards, notes, workflowCards, workflowsHub],
|
||||
[cards, viewCards, browserCards, workflowCards, workflowsHub],
|
||||
);
|
||||
|
||||
const handleCanvasMouseDown = useCallback(
|
||||
|
||||
@@ -33,8 +33,6 @@ export function useDashboardSelectors(dashboardId: string) {
|
||||
const pendingFocusWorkflowsHub = useAppSelector((state) => state.dashboardLayout.pendingFocusWorkflowsHub);
|
||||
const workflowItems = useAppSelector((state) => state.workflows.items);
|
||||
const workflowOpenCards = useAppSelector((state) => state.workflows.openCards);
|
||||
const notes = useAppSelector((state) => state.dashboardLayout.notes);
|
||||
const pendingFocusNoteId = useAppSelector((state) => state.dashboardLayout.pendingFocusNoteId);
|
||||
const layoutInitialized = useAppSelector((state) => state.dashboardLayout.initialized);
|
||||
const persistedExpandedSessionIds = useAppSelector((state) => state.dashboardLayout.persistedExpandedSessionIds);
|
||||
const zoomSensitivity = useAppSelector((state) => state.settings.data.zoom_sensitivity);
|
||||
@@ -61,8 +59,6 @@ export function useDashboardSelectors(dashboardId: string) {
|
||||
workflowsHub,
|
||||
pendingFocusWorkflowId,
|
||||
pendingFocusWorkflowsHub,
|
||||
notes,
|
||||
pendingFocusNoteId,
|
||||
layoutInitialized,
|
||||
persistedExpandedSessionIds,
|
||||
zoomSensitivity,
|
||||
|
||||
@@ -14,13 +14,11 @@ function dashboardSignature(s: {
|
||||
cards: Record<string, unknown>;
|
||||
viewCards: Record<string, unknown>;
|
||||
browserCards: Record<string, unknown>;
|
||||
notes: Record<string, unknown>;
|
||||
}): string {
|
||||
return [
|
||||
...Object.keys(s.cards),
|
||||
...Object.keys(s.viewCards),
|
||||
...Object.keys(s.browserCards),
|
||||
...Object.keys(s.notes),
|
||||
].sort().join(',');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type CardPosition,
|
||||
type ViewCardPosition,
|
||||
type BrowserCardPosition,
|
||||
type NotePosition,
|
||||
type WorkflowCardPosition,
|
||||
type WorkflowsHubPosition,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -19,7 +18,6 @@ interface UseLayoutSaveArgs {
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
notes: Record<string, NotePosition>;
|
||||
expandedSessionIds: string[];
|
||||
captureNow: () => void;
|
||||
}
|
||||
@@ -34,7 +32,6 @@ export function useLayoutSave({
|
||||
browserCards,
|
||||
workflowCards,
|
||||
workflowsHub,
|
||||
notes,
|
||||
expandedSessionIds,
|
||||
captureNow,
|
||||
}: UseLayoutSaveArgs) {
|
||||
@@ -50,7 +47,7 @@ export function useLayoutSave({
|
||||
skipInitialSave.current = false;
|
||||
return;
|
||||
}
|
||||
const payload = { dashboardId, cards, viewCards, browserCards, workflowCards, workflowsHub, notes, expandedSessionIds };
|
||||
const payload = { dashboardId, cards, viewCards, browserCards, workflowCards, workflowsHub, expandedSessionIds };
|
||||
pendingSaveRef.current = payload;
|
||||
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
||||
saveTimerRef.current = setTimeout(() => {
|
||||
@@ -59,7 +56,7 @@ export function useLayoutSave({
|
||||
saveTimerRef.current = null;
|
||||
captureNow();
|
||||
}, 500);
|
||||
}, [isActive, cards, viewCards, browserCards, workflowCards, workflowsHub, notes, expandedSessionIds, layoutInitialized, dashboardId, dispatch, captureNow]);
|
||||
}, [isActive, cards, viewCards, browserCards, workflowCards, workflowsHub, expandedSessionIds, layoutInitialized, dashboardId, dispatch, captureNow]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const WORKFLOW_CARD_GAP = 140;
|
||||
const GRID_ORIGIN = { x: 40, y: 100 };
|
||||
const GRID_COLS_FALLBACK = 4;
|
||||
|
||||
export type CardType = 'agent' | 'view' | 'browser' | 'note' | 'workflow' | 'workflows-hub' | 'workflows-monitor';
|
||||
export type CardType = 'agent' | 'view' | 'browser' | 'workflow' | 'workflows-hub' | 'workflows-monitor';
|
||||
|
||||
export interface CardPosition {
|
||||
session_id: string;
|
||||
@@ -111,29 +111,11 @@ export interface WorkflowsHubPosition {
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export type NoteColor = 'yellow' | 'pink' | 'blue' | 'green' | 'purple' | 'gray';
|
||||
|
||||
export interface NotePosition {
|
||||
note_id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
content: string;
|
||||
color: NoteColor;
|
||||
zOrder: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_NOTE_W = 240;
|
||||
export const DEFAULT_NOTE_H = 200;
|
||||
|
||||
// One entry in the Ctrl/Cmd+Shift+T "reopen last closed" stack: a full snapshot for browser/view/workflow/note/tab, just the session id for an agent (its session is brought back via resumeSession).
|
||||
// One entry in the Ctrl/Cmd+Shift+T "reopen last closed" stack: a full snapshot for browser/view/workflow/tab, just the session id for an agent (its session is brought back via resumeSession).
|
||||
export type ClosedCard =
|
||||
| { uid: string; kind: 'browser'; closedAt: number; card: BrowserCardPosition }
|
||||
| { uid: string; kind: 'view'; closedAt: number; card: ViewCardPosition }
|
||||
| { uid: string; kind: 'workflow'; closedAt: number; card: WorkflowCardPosition }
|
||||
| { uid: string; kind: 'note'; closedAt: number; note: NotePosition }
|
||||
| { uid: string; kind: 'tab'; closedAt: number; browserId: string; index: number; tab: BrowserTab }
|
||||
| { uid: string; kind: 'agent'; closedAt: number; sessionId: string; position: CardPosition | null };
|
||||
|
||||
@@ -147,13 +129,12 @@ export interface DashboardLayoutState {
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
notes: Record<string, NotePosition>;
|
||||
closedCardPositions: Record<string, CardPosition>;
|
||||
/** Session-global LIFO undo stack for Ctrl/Cmd+Shift+T; survives dashboard switches (resetLayout leaves it alone). */
|
||||
recentlyClosed: ClosedCard[];
|
||||
glowingBrowserCards: Record<string, { sourceId: string; fading: boolean; label?: string }>;
|
||||
glowingAgentCards: Record<string, { sourceId: string; fading: boolean; sourceYRatio?: number; label?: string }>;
|
||||
/** Window controls: cards collapsed to a title pill (many at once). Keyed by any card id (session/note/browser/view/workflow). */
|
||||
/** Window controls: cards collapsed to a title pill (many at once). Keyed by any card id (session/browser/view/workflow). */
|
||||
minimizedCards: Record<string, boolean>;
|
||||
/** macOS-style tiling: card id -> zone ('fullscreen' | 'fill' | 'left'|'right'|'top'|'bottom' | 'tl'|'tr'|'bl'|'br' | 't3l'|'t3c'|'t3r'). A tiled card renders at that viewport region (webview stays mounted); 'fullscreen' also hides the app chrome. */
|
||||
tiledCards: Record<string, string>;
|
||||
@@ -167,7 +148,6 @@ export interface DashboardLayoutState {
|
||||
pendingFocusBrowserId: string | null;
|
||||
// Set when a view card is opened from outside the canvas (sidebar app click / toolbar picker) so the dashboard fits+highlights it on arrival; holds the card key.
|
||||
pendingFocusViewCardId: string | null;
|
||||
pendingFocusNoteId: string | null;
|
||||
/** Transient: snapshot stand-ins for off-screen webviews; never rides the layout PUT. */
|
||||
suspendedBrowserCards: Record<string, { dataUrl: string; capturedAt: number }>;
|
||||
/** Transient: spawned cards that are about to be removed; surfaces the fade + Keep pill. */
|
||||
@@ -203,7 +183,6 @@ const initialState: DashboardLayoutState = {
|
||||
browserCards: {},
|
||||
workflowCards: {},
|
||||
workflowsHub: null,
|
||||
notes: {},
|
||||
closedCardPositions: {},
|
||||
recentlyClosed: [],
|
||||
glowingBrowserCards: {},
|
||||
@@ -217,7 +196,6 @@ const initialState: DashboardLayoutState = {
|
||||
saveArmed: false,
|
||||
pendingFocusBrowserId: null,
|
||||
pendingFocusViewCardId: null,
|
||||
pendingFocusNoteId: null,
|
||||
suspendedBrowserCards: {},
|
||||
endingBrowserCards: {},
|
||||
activeViewCardId: null,
|
||||
@@ -236,7 +214,6 @@ interface LayoutPayload {
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
workflowCards: Record<string, WorkflowCardPosition>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
notes: Record<string, NotePosition>;
|
||||
expandedSessionIds: string[];
|
||||
}
|
||||
|
||||
@@ -273,7 +250,6 @@ export const fetchLayout = createAsyncThunk(
|
||||
browserCards: browserCards as Record<string, BrowserCardPosition>,
|
||||
workflowCards: (layout.workflow_cards ?? {}) as Record<string, WorkflowCardPosition>,
|
||||
workflowsHub: (layout.workflows_hub ?? null) as WorkflowsHubPosition | null,
|
||||
notes: (layout.notes ?? {}) as Record<string, NotePosition>,
|
||||
expandedSessionIds: (layout.expanded_session_ids ?? []) as string[],
|
||||
} satisfies LayoutPayload;
|
||||
},
|
||||
@@ -299,7 +275,6 @@ export const saveLayout = createAsyncThunk(
|
||||
browser_cards: payload.browserCards,
|
||||
workflow_cards: payload.workflowCards,
|
||||
workflows_hub: payload.workflowsHub,
|
||||
notes: payload.notes,
|
||||
expanded_session_ids: payload.expandedSessionIds,
|
||||
},
|
||||
}),
|
||||
@@ -350,10 +325,6 @@ function collectOccupiedRects(
|
||||
if (state.workflowsHub) {
|
||||
rects.push({ x: state.workflowsHub.x, y: state.workflowsHub.y, w: state.workflowsHub.width, h: state.workflowsHub.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;
|
||||
}
|
||||
|
||||
@@ -527,7 +498,7 @@ export function placeInParentColumn(
|
||||
return placeBesideCard(state, parentCard, newW, newH, expandedSessionIds, exclude);
|
||||
}
|
||||
|
||||
// Where a user-created card (chat/app/browser/note) should land. Resolved in the UI layer where selection + viewport are known, then handed to the add reducers as an explicit x/y. `beside` (the currently selected card) docks the new card to its right, stacking under that column (collision-aware); `viewportCenter` (canvas-space center of what the user is looking at) drops it dead-center "in front of you", overlapping whatever's there. With neither, falls back to the legacy top-left grid scan.
|
||||
// Where a user-created card (chat/app/browser) should land. Resolved in the UI layer where selection + viewport are known, then handed to the add reducers as an explicit x/y. `beside` (the currently selected card) docks the new card to its right, stacking under that column (collision-aware); `viewportCenter` (canvas-space center of what the user is looking at) drops it dead-center "in front of you", overlapping whatever's there. With neither, falls back to the legacy top-left grid scan.
|
||||
export interface SpawnAnchor {
|
||||
beside?: { x: number; y: number; width: number; height: number };
|
||||
viewportCenter?: { x: number; y: number };
|
||||
@@ -677,12 +648,10 @@ const dashboardLayoutSlice = createSlice({
|
||||
for (const c of Object.values(state.viewCards)) tally(c.zOrder);
|
||||
for (const c of Object.values(state.browserCards)) tally(c.zOrder);
|
||||
for (const c of Object.values(state.workflowCards)) tally(c.zOrder);
|
||||
for (const n of Object.values(state.notes)) tally(n.zOrder);
|
||||
if (state.workflowsHub) tally(state.workflowsHub.zOrder);
|
||||
if (state.workflowsMonitorCard) tally(state.workflowsMonitorCard.zOrder);
|
||||
if (type === 'agent') currentZ = state.cards[id]?.zOrder ?? 0;
|
||||
else if (type === 'view') currentZ = state.viewCards[id]?.zOrder ?? 0;
|
||||
else if (type === 'note') currentZ = state.notes[id]?.zOrder ?? 0;
|
||||
else if (type === 'workflow') currentZ = state.workflowCards[id]?.zOrder ?? 0;
|
||||
else if (type === 'workflows-hub') currentZ = state.workflowsHub?.zOrder ?? 0;
|
||||
else if (type === 'workflows-monitor') currentZ = state.workflowsMonitorCard?.zOrder ?? 0;
|
||||
@@ -696,9 +665,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
} else if (type === 'view') {
|
||||
const card = state.viewCards[id];
|
||||
if (card) card.zOrder = z;
|
||||
} else if (type === 'note') {
|
||||
const note = state.notes[id];
|
||||
if (note) note.zOrder = z;
|
||||
} else if (type === 'workflow') {
|
||||
const card = state.workflowCards[id];
|
||||
if (card) card.zOrder = z;
|
||||
@@ -1472,12 +1438,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
card.x += dx;
|
||||
card.y += dy;
|
||||
}
|
||||
} else if (item.type === 'note') {
|
||||
const note = state.notes[item.id];
|
||||
if (note) {
|
||||
note.x += dx;
|
||||
note.y += dy;
|
||||
}
|
||||
} else if (item.type === 'workflow') {
|
||||
const card = state.workflowCards[item.id];
|
||||
if (card) {
|
||||
@@ -1499,67 +1459,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
addNote(
|
||||
state,
|
||||
action: PayloadAction<{ x?: number; y?: number; expandedSessionIds?: string[]; color?: NoteColor; content?: string }>,
|
||||
) {
|
||||
const id = `note-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
let posX: number, posY: number;
|
||||
if (action.payload.x != null && action.payload.y != null) {
|
||||
posX = action.payload.x;
|
||||
posY = action.payload.y;
|
||||
} else {
|
||||
const rects = collectOccupiedRects(state, action.payload.expandedSessionIds);
|
||||
const pos = findOpenGridCell(rects, DEFAULT_NOTE_W, DEFAULT_NOTE_H);
|
||||
posX = pos.x;
|
||||
posY = pos.y;
|
||||
}
|
||||
state.notes[id] = {
|
||||
note_id: id,
|
||||
x: posX,
|
||||
y: posY,
|
||||
width: DEFAULT_NOTE_W,
|
||||
height: DEFAULT_NOTE_H,
|
||||
content: action.payload.content ?? '',
|
||||
color: action.payload.color || 'yellow',
|
||||
zOrder: state.nextZOrder++,
|
||||
};
|
||||
state.pendingFocusNoteId = id;
|
||||
},
|
||||
|
||||
setNotePosition(state, action: PayloadAction<{ noteId: string; x: number; y: number }>) {
|
||||
const n = state.notes[action.payload.noteId];
|
||||
if (n) { n.x = action.payload.x; n.y = action.payload.y; }
|
||||
},
|
||||
|
||||
setNoteSize(state, action: PayloadAction<{ noteId: string; width: number; height: number }>) {
|
||||
const n = state.notes[action.payload.noteId];
|
||||
if (n) {
|
||||
n.width = Math.max(160, action.payload.width);
|
||||
n.height = Math.max(120, action.payload.height);
|
||||
}
|
||||
},
|
||||
|
||||
updateNoteContent(state, action: PayloadAction<{ noteId: string; content: string }>) {
|
||||
const n = state.notes[action.payload.noteId];
|
||||
if (n) n.content = action.payload.content;
|
||||
},
|
||||
|
||||
setNoteColor(state, action: PayloadAction<{ noteId: string; color: NoteColor }>) {
|
||||
const n = state.notes[action.payload.noteId];
|
||||
if (n) n.color = action.payload.color;
|
||||
},
|
||||
|
||||
removeNote(state, action: PayloadAction<string>) {
|
||||
delete state.notes[action.payload];
|
||||
delete state.tiledCards[action.payload];
|
||||
delete state.minimizedCards[action.payload];
|
||||
},
|
||||
|
||||
clearPendingFocusNoteId(state) {
|
||||
state.pendingFocusNoteId = null;
|
||||
},
|
||||
|
||||
// Snapshot a card onto the reopen stack RIGHT BEFORE it's closed (the data must still be in state). Dispatch only from genuine user closes, not programmatic teardown.
|
||||
recordClosedCard(
|
||||
state,
|
||||
@@ -1575,8 +1474,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
entry = { uid, kind, closedAt, card: { ...state.viewCards[id] } };
|
||||
} else if (kind === 'workflow' && state.workflowCards[id]) {
|
||||
entry = { uid, kind, closedAt, card: { ...state.workflowCards[id] } };
|
||||
} else if (kind === 'note' && state.notes[id]) {
|
||||
entry = { uid, kind, closedAt, note: { ...state.notes[id] } };
|
||||
} else if (kind === 'agent') {
|
||||
entry = { uid, kind, closedAt, sessionId: id, position: state.cards[id] ? { ...state.cards[id] } : null };
|
||||
} else if (kind === 'tab' && browserId && state.browserCards[browserId]) {
|
||||
@@ -1603,8 +1500,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.viewCards[viewCardKey(entry.card.output_id, entry.card.instance)] = { ...entry.card, zOrder };
|
||||
} else if (entry.kind === 'workflow') {
|
||||
state.workflowCards[entry.card.workflow_id] = { ...entry.card, zOrder };
|
||||
} else if (entry.kind === 'note') {
|
||||
state.notes[entry.note.note_id] = { ...entry.note, zOrder };
|
||||
} else if (entry.kind === 'tab') {
|
||||
const card = state.browserCards[entry.browserId];
|
||||
if (card) {
|
||||
@@ -1699,7 +1594,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.browserCards = keptBrowsers;
|
||||
state.workflowCards = {};
|
||||
state.workflowsHub = null;
|
||||
state.notes = {};
|
||||
state.closedCardPositions = {};
|
||||
state.glowingBrowserCards = {};
|
||||
state.glowingAgentCards = {};
|
||||
@@ -1707,7 +1601,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.nextZOrder = 1;
|
||||
state.initialized = false;
|
||||
state.saveArmed = false;
|
||||
state.pendingFocusNoteId = null;
|
||||
state.suspendedBrowserCards = keptSuspended;
|
||||
state.endingBrowserCards = {};
|
||||
state.pendingFocusWorkflowId = null;
|
||||
@@ -1743,7 +1636,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.browserCards = { ...incoming, ...keptAlive };
|
||||
state.workflowCards = action.payload.workflowCards || {};
|
||||
state.workflowsHub = action.payload.workflowsHub || null;
|
||||
state.notes = action.payload.notes || {};
|
||||
} else {
|
||||
const occupied = collectOccupiedRects(state, action.payload.expandedSessionIds);
|
||||
addMissingCards(state.cards, action.payload.cards, occupied);
|
||||
@@ -1754,7 +1646,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
}
|
||||
addMissingCards(state.workflowCards, action.payload.workflowCards || {}, occupied);
|
||||
if (!state.workflowsHub && action.payload.workflowsHub) state.workflowsHub = action.payload.workflowsHub;
|
||||
addMissingCards(state.notes, action.payload.notes || {}, occupied);
|
||||
}
|
||||
state.persistedExpandedSessionIds = action.payload.expandedSessionIds;
|
||||
|
||||
@@ -1775,10 +1666,6 @@ const dashboardLayoutSlice = createSlice({
|
||||
if (!w.zOrder) w.zOrder = 0;
|
||||
if (w.zOrder > maxZ) maxZ = w.zOrder;
|
||||
}
|
||||
for (const n of Object.values(state.notes)) {
|
||||
if (!n.zOrder) n.zOrder = 0;
|
||||
if (n.zOrder > maxZ) maxZ = n.zOrder;
|
||||
}
|
||||
state.nextZOrder = maxZ + 1;
|
||||
})
|
||||
.addCase(fetchLayout.rejected, (state) => {
|
||||
@@ -1903,13 +1790,6 @@ export const {
|
||||
setWorkflowsHubPosition,
|
||||
setWorkflowsHubSize,
|
||||
clearPendingFocusWorkflowsHub,
|
||||
addNote,
|
||||
setNotePosition,
|
||||
setNoteSize,
|
||||
updateNoteContent,
|
||||
setNoteColor,
|
||||
removeNote,
|
||||
clearPendingFocusNoteId,
|
||||
recordClosedCard,
|
||||
restoreClosedCard,
|
||||
popClosedCard,
|
||||
@@ -1942,7 +1822,7 @@ export const selectFullscreenCardId = (state: { dashboardLayout: DashboardLayout
|
||||
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;
|
||||
const exists = id in s.cards || id in s.viewCards || id in s.browserCards || id in s.workflowCards;
|
||||
return exists ? id : null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user