Merge branch 'eric/dev' into eric/redesign

# Conflicts:
#	backend/apps/agents/manager/run/RunOptions.py
#	electron/main.js
#	frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx
#	frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx
#	frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx
#	frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts
This commit is contained in:
ciregenz
2026-07-19 21:40:18 -07:00
99 changed files with 2835 additions and 689 deletions
@@ -272,7 +272,9 @@ const AppShell: React.FC = () => {
dispatch(setPendingBrowserUrl(url));
const lastId = (window as any).__openswarm_last_dashboard_id as string | undefined;
const firstDashboard = dashboardList[0];
const targetId = lastId || firstDashboard?.id;
// Only navigate to lastId if it's a REAL dashboard: a stale localStorage id for a deleted dashboard used to route to /dashboard/<phantom>, which 404s and re-fires the layout wipe (drops your cards / breaks a drag).
const lastIsReal = !!lastId && dashboardList.some((d) => d.id === lastId);
const targetId = (lastIsReal ? lastId : undefined) || firstDashboard?.id;
if (targetId) {
navigate(`/dashboard/${targetId}`);
} else {
@@ -9,6 +9,8 @@ import FileDownloadIcon from '@mui/icons-material/FileDownload';
import { useNavigate } from 'react-router-dom';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { fetchWorkflows } from '@/shared/state/workflowsSlice';
import ImportDigest, { DigestHandle } from './ImportDigest';
import ImportModal from './ImportModal';
@@ -43,6 +45,8 @@ const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
const ImportEntryPoint: React.FC = () => {
const c = useClaudeTokens();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const dashboardId = useAppSelector((s) => s.tempState.lastDashboardId) || undefined;
const inputRef = useRef<HTMLInputElement | null>(null);
const digestRef = useRef<DigestHandle | null>(null);
const depth = useRef(0);
@@ -56,10 +60,12 @@ const ImportEntryPoint: React.FC = () => {
(rootType: string, rootId: string, name: string) => {
const msg = rootType === 'app' ? `Added ${name} to your Apps` : `Added ${name}`;
setToast({ msg, sev: 'success' });
// A workflow has no route of its own, so nothing would pull it in: an open Workflows hub only fetches on mount and would keep showing a stale list. Import drops dashboard_id, and /list keeps unassigned workflows for every dashboard, so this surfaces it wherever the user is.
if (rootType === 'workflow') dispatch(fetchWorkflows(dashboardId));
const to = DEST[rootType]?.(rootId);
if (to) navigate(to);
},
[navigate],
[navigate, dispatch, dashboardId],
);
const commitAndFinish = useCallback(
@@ -55,11 +55,11 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
return load();
}, [open, load]);
const handleDownload = async () => {
const handleDownload = async (allowSecrets = false) => {
if (!preflight) return;
setDownloading(true);
try {
await downloadSwarm(target, preflight.filename);
await downloadSwarm(target, preflight.filename, allowSecrets);
setToast(`Saved ${preflight.filename}`);
onClose();
} catch (e: any) {
@@ -68,6 +68,8 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
setDownloading(false);
}
};
// The file-content secret heuristic is overridable (download goes to people you trust); our own credential fields ("secret-shaped field(s)") are not.
const secretOverridable = error.includes('secret-shaped value');
const optionRow = (
selected: boolean,
@@ -150,6 +152,16 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
<Button size="small" onClick={load} sx={{ textTransform: 'none', color: c.accent.primary }}>
Try again
</Button>
{secretOverridable && (
<Button
size="small"
onClick={() => { setError(''); handleDownload(true); }}
disabled={downloading}
sx={{ textTransform: 'none', color: c.status.error, ml: 1 }}
>
Export anyway (includes the flagged value; only send to people you trust)
</Button>
)}
</Box>
) : preflight ? (
<IncludesList summary={preflight.summary} />
@@ -179,7 +191,7 @@ const ShareModal: React.FC<Props> = ({ target, open, onClose }) => {
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Button
variant="contained"
onClick={handleDownload}
onClick={() => handleDownload()}
disabled={!preflight || downloading}
startIcon={
downloading ? (
@@ -28,11 +28,11 @@ export async function exportPreflight(target: ShareTarget): Promise<ExportPrefli
return res.json();
}
export async function downloadSwarm(target: ShareTarget, filename: string): Promise<void> {
export async function downloadSwarm(target: ShareTarget, filename: string, allowSecrets = false): Promise<void> {
const res = await fetch(`${API_BASE}/swarm/export`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: target.kind, id: target.id }),
body: JSON.stringify({ type: target.kind, id: target.id, allow_secrets: allowSecrets }),
});
if (!res.ok) throw new Error(await _detail(res, "We couldn't build the file."));
const blob = await res.blob();