mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[eric] small bug fixes
This commit is contained in:
@@ -186,7 +186,6 @@ FULL_TOOLS = [
|
||||
"WebSearch", "WebFetch", "NotebookEdit", "TodoWrite",
|
||||
"EnterPlanMode", "ExitPlanMode", "EnterWorktree",
|
||||
"TaskOutput", "TaskStop",
|
||||
"CronCreate", "CronList", "CronDelete",
|
||||
"InvokeAgent",
|
||||
"Agent",
|
||||
# ToolSearch is the loader the CLI uses to expose deferred tool schemas
|
||||
@@ -667,6 +666,15 @@ class AgentManager:
|
||||
"Calendar/Drive, the equivalent OpenSwarm server is listed below; "
|
||||
"activate that one via MCPActivate instead."
|
||||
)
|
||||
sections.append(
|
||||
"1b. NEVER call CronCreate, CronList, CronDelete, ScheduleWakeup, "
|
||||
"PushNotification, RemoteTrigger, or any Task* tool. Those are "
|
||||
"claude.ai Routines/Tasks; OpenSwarm has its own scheduler that "
|
||||
"the user drives by clicking 'Schedule this task' on the chat "
|
||||
"card. If the user asks to schedule something, do the work once, "
|
||||
"then tell them to click that button. Do not propose a routine, "
|
||||
"do not say 'I'll schedule it', do not call any scheduling tool."
|
||||
)
|
||||
sections.append(
|
||||
"2. After MCPActivate returns, end the turn; a follow-up turn fires "
|
||||
"automatically with the new tools available."
|
||||
@@ -2305,8 +2313,25 @@ class AgentManager:
|
||||
# here, and confuse the model into picking the partner shim
|
||||
# instead of our vetted server. Hard-block them at the SDK
|
||||
# layer so the model can't even attempt the call.
|
||||
# claude.ai partner shims and claude.ai's Routines/Tasks
|
||||
# product compete with OpenSwarm's own MCP gate and workflow
|
||||
# scheduler. Block them at the SDK so the model can't reach
|
||||
# for them even when it's tempted. OpenSwarm scheduling is
|
||||
# user-initiated via the "Schedule this task" UI, not
|
||||
# something the agent calls a tool to set up.
|
||||
options_kwargs["disallowed_tools"] = [
|
||||
"mcp__claude_ai_*",
|
||||
"Skill",
|
||||
"CronCreate",
|
||||
"CronList",
|
||||
"CronDelete",
|
||||
"PushNotification",
|
||||
"RemoteTrigger",
|
||||
"ScheduleWakeup",
|
||||
"TaskCreate",
|
||||
"TaskGet",
|
||||
"TaskList",
|
||||
"TaskUpdate",
|
||||
]
|
||||
|
||||
if session.cwd:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useMemo, useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Chip from '@mui/material/Chip';
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
fetchSession,
|
||||
AgentMessage,
|
||||
clearSessionMessages,
|
||||
clearMcpSuggestions,
|
||||
} from '@/shared/state/agentsSlice';
|
||||
import { fetchModes } from '@/shared/state/modesSlice';
|
||||
import { createSessionWs } from '@/shared/ws/WebSocketManager';
|
||||
@@ -170,6 +171,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
};
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
const id = sessionIdProp || routeId;
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const session = useAppSelector((state) => (id ? state.agents.sessions[id] : undefined));
|
||||
const modesMap = useAppSelector((state) => state.modes.items);
|
||||
@@ -1111,8 +1113,32 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
borderRadius: 1.5,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
bgcolor: c.bg.secondary,
|
||||
position: 'relative',
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: c.text.primary, fontWeight: 500, mb: 0.5 }}>
|
||||
<Box
|
||||
role="button"
|
||||
aria-label="Dismiss integration suggestion"
|
||||
onClick={() => id && dispatch(clearMcpSuggestions({ sessionId: id }))}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1,
|
||||
color: c.text.muted,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 0.75,
|
||||
'&:hover': { color: c.text.primary, bgcolor: c.bg.elevated },
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: c.text.primary, fontWeight: 500, mb: 0.5, pr: 3 }}>
|
||||
Looks like this might need an integration
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: c.text.secondary, display: 'block', mb: 1 }}>
|
||||
@@ -1152,8 +1178,18 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
parent_session_id: session.id,
|
||||
}),
|
||||
});
|
||||
const body = await r.json().catch(() => ({} as any));
|
||||
if (!r.ok) {
|
||||
setActivateError(`Activation failed (${r.status})`);
|
||||
} else if (body?.status === 'unknown_server') {
|
||||
// Not yet connected; jump straight to Actions
|
||||
// so the user can finish OAuth. Nothing here
|
||||
// can do it on their behalf.
|
||||
navigate('/actions');
|
||||
} else if (id) {
|
||||
// Activation succeeded; clear the banner so the user
|
||||
// gets visual confirmation the click did something.
|
||||
dispatch(clearMcpSuggestions({ sessionId: id }));
|
||||
}
|
||||
} catch (e: any) {
|
||||
setActivateError(e?.message || 'Activation failed');
|
||||
@@ -1340,30 +1376,6 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
seedKey={`${session.id}:${session.messages?.length ?? 0}`}
|
||||
/>
|
||||
)}
|
||||
{scheduleSuggestion && id && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', my: 0.75, ml: 1 }}>
|
||||
<Box
|
||||
onClick={(e) => setScheduleAnchor(e.currentTarget as HTMLElement)}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.6,
|
||||
px: 1.1, py: 0.55,
|
||||
fontSize: '0.78rem', fontWeight: 600,
|
||||
color: c.accent.primary,
|
||||
bgcolor: c.accent.primary + '14',
|
||||
border: `1px solid ${c.accent.primary}40`,
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: c.accent.primary + '22' },
|
||||
}}>
|
||||
<ScheduleIcon sx={{ fontSize: 14 }} />
|
||||
Schedule: {scheduleSuggestion.presetLabel}
|
||||
<Box
|
||||
onClick={(e) => { e.stopPropagation(); setSuggestDismissedFor(scheduleSuggestion.messageId); }}
|
||||
sx={{ ml: 0.4, color: c.text.muted, fontSize: '0.8rem', '&:hover': { color: c.text.primary } }}>×</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{showResumeBubble && session.status === 'stopped' && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', my: 0.75 }}>
|
||||
<Box
|
||||
|
||||
@@ -295,6 +295,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
const isDashboardActive = useDashboardActive();
|
||||
const hasApiKey = !!useAppSelector((s) => s.settings.data.anthropic_api_key);
|
||||
const modelsByProvider = useAppSelector((s) => s.models.byProvider);
|
||||
const expandedSessionIds = useAppSelector((s) => s.agents.expandedSessionIds);
|
||||
// Curated picker label with a tidy fallback for unknowns.
|
||||
const friendlyModelLabel = useMemo(() => {
|
||||
const value = session.model;
|
||||
@@ -945,6 +946,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
dispatch(addWorkflowCard({
|
||||
workflowId: tempId,
|
||||
sourceSessionId: session.id,
|
||||
expandedSessionIds,
|
||||
}));
|
||||
dispatch(openWorkflowCard({
|
||||
workflowId: tempId,
|
||||
|
||||
@@ -4,11 +4,6 @@ import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import EditIcon from '@mui/icons-material/EditOutlined';
|
||||
import HistoryIcon from '@mui/icons-material/HistoryRounded';
|
||||
@@ -20,7 +15,6 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
closeWorkflowCard,
|
||||
deleteWorkflow,
|
||||
fetchRuns,
|
||||
openWorkflowCard as openWorkflowCardAction,
|
||||
rekeyOpenCard,
|
||||
@@ -30,15 +24,21 @@ import {
|
||||
type Workflow,
|
||||
} from '@/shared/state/workflowsSlice';
|
||||
import {
|
||||
DEFAULT_CARD_H,
|
||||
DEFAULT_CARD_W,
|
||||
placeCard,
|
||||
rekeyWorkflowCard,
|
||||
removeWorkflowCard,
|
||||
setWorkflowCardPosition,
|
||||
setWorkflowCardSize,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { setPendingFocusAgentId } from '@/shared/state/tempStateSlice';
|
||||
import { fetchSession } from '@/shared/state/agentsSlice';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import WorkflowEditViews from './WorkflowEditViews';
|
||||
import { HistoryDetail, HistoryList, PreviewView, SavedView } from './WorkflowCardSubviews';
|
||||
import { StatusDot, RunSparkline, LastFiredHint, isStaleSinceLastRun } from './workflowVisuals';
|
||||
import { store } from '@/shared/state/store';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
@@ -99,6 +99,7 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
const card = useAppSelector((s) => s.workflows.openCards[workflowId]);
|
||||
const workflow = useAppSelector((s) => s.workflows.items[workflowId]);
|
||||
const runs = useAppSelector((s) => s.workflows.runs[workflowId]);
|
||||
const expandedSessionIds = useAppSelector((s) => s.agents.expandedSessionIds);
|
||||
// Transient "Starting…" label state on the Run button. See onClick handler
|
||||
// for the full rationale (avoid no-feedback flicker on fast manual runs).
|
||||
const [runStarting, setRunStarting] = useState(false);
|
||||
@@ -329,34 +330,13 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
}, [computeResize, dispatch, workflowId]);
|
||||
|
||||
// ---- Close: drop transient view state AND remove from layout ----
|
||||
// Two-step when the schedule is on: a quiet X would make the workflow
|
||||
// a "ghost" (still firing on a hidden timer) which surprises users who
|
||||
// mentally model X as "throw away." Confirm-then-act lets them choose
|
||||
// between hiding the card and actually killing the schedule.
|
||||
const [closeConfirmOpen, setCloseConfirmOpen] = useState(false);
|
||||
const hardClose = useCallback(() => {
|
||||
// X just hides the card. Schedule keeps firing in the background; the
|
||||
// user can re-open from the Workflows hub. A confirm dialog here was
|
||||
// more friction than value (users clicked through it without reading).
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(closeWorkflowCard(workflowId));
|
||||
dispatch(removeWorkflowCard(workflowId));
|
||||
}, [dispatch, workflowId]);
|
||||
const onClose = useCallback(() => {
|
||||
if (workflow?.schedule?.enabled) {
|
||||
setCloseConfirmOpen(true);
|
||||
return;
|
||||
}
|
||||
hardClose();
|
||||
}, [workflow?.schedule?.enabled, hardClose]);
|
||||
const onConfirmHide = useCallback(() => {
|
||||
setCloseConfirmOpen(false);
|
||||
hardClose();
|
||||
}, [hardClose]);
|
||||
const onConfirmStopAndDelete = useCallback(async () => {
|
||||
setCloseConfirmOpen(false);
|
||||
if (workflow?.id) {
|
||||
await dispatch(deleteWorkflow(workflow.id));
|
||||
}
|
||||
hardClose();
|
||||
}, [dispatch, workflow?.id, hardClose]);
|
||||
|
||||
// ---- Display calculations ----
|
||||
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
|
||||
@@ -369,9 +349,6 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
|
||||
if (!card) return null;
|
||||
|
||||
// A "running" run is one that's actively executing right now. While
|
||||
// running, the card grows a subtle conic-gradient halo + a faint title
|
||||
// pulse so a glance at the canvas tells you something's working.
|
||||
const isRunning = (runs || []).some((r) => r.status === 'running') || workflow?.last_run_status === 'running';
|
||||
|
||||
// Hairline border for the default idle state (item #19 in target #54
|
||||
@@ -399,7 +376,6 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
data-select-type="workflow-card"
|
||||
data-select-id={workflowId}
|
||||
data-select-meta={JSON.stringify({ name: title })}
|
||||
data-running={isRunning ? 'true' : undefined}
|
||||
onPointerDownCapture={() => onBringToFront?.(workflowId, 'workflow')}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
if (justDraggedRef.current) return;
|
||||
@@ -426,43 +402,6 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
zIndex: (isDragging || isResizing) ? 999999 : cardZOrder,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
|
||||
'&:hover .resize-handle': { opacity: 1 },
|
||||
// Running halo: conic-gradient sweep around the card border + a
|
||||
// faint inner glow. Lives on ::before so the card body stays
|
||||
// crisp and isn't redrawn each frame. Only renders when the
|
||||
// data-running attribute is set (no perf cost when idle).
|
||||
'&[data-running="true"]::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: -1,
|
||||
borderRadius: '15px',
|
||||
padding: '1.5px',
|
||||
background: `conic-gradient(from 0deg, transparent 0deg, ${c.accent.primary} 60deg, transparent 120deg, transparent 240deg, ${c.accent.primary} 300deg, transparent 360deg)`,
|
||||
WebkitMask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)',
|
||||
WebkitMaskComposite: 'xor',
|
||||
maskComposite: 'exclude',
|
||||
animation: 'workflowRunSweep 2.4s linear infinite',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
opacity: 0.85,
|
||||
},
|
||||
'&[data-running="true"]::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: '14px',
|
||||
background: `radial-gradient(120% 80% at 50% 0%, ${c.accent.primary}10 0%, transparent 60%)`,
|
||||
animation: 'workflowRunPulse 2.4s ease-in-out infinite',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
},
|
||||
'@keyframes workflowRunSweep': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
'@keyframes workflowRunPulse': {
|
||||
'0%, 100%': { opacity: 0.5 },
|
||||
'50%': { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* ===== Title bar / drag handle =====
|
||||
@@ -664,7 +603,31 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
{card.view === 'history' && workflow && (
|
||||
<HistoryList
|
||||
runs={runs || []}
|
||||
onOpen={(run) => dispatch(updateWorkflowCard({ workflowId, patch: { view: 'history_detail', historyRunId: run.id } }))}
|
||||
onOpen={async (run) => {
|
||||
if (!run.session_id) {
|
||||
dispatch(updateWorkflowCard({ workflowId, patch: { view: 'history_detail', historyRunId: run.id } }));
|
||||
return;
|
||||
}
|
||||
const sid = run.session_id;
|
||||
if (!store.getState().agents.sessions[sid]) {
|
||||
try { await dispatch(fetchSession(sid)).unwrap(); } catch { /* fall back to detail */ }
|
||||
}
|
||||
if (!store.getState().agents.sessions[sid]) {
|
||||
dispatch(updateWorkflowCard({ workflowId, patch: { view: 'history_detail', historyRunId: run.id } }));
|
||||
return;
|
||||
}
|
||||
if (!store.getState().dashboardLayout.cards[sid]) {
|
||||
dispatch(placeCard({
|
||||
sessionId: sid,
|
||||
x: cardX + cardWidth + 60,
|
||||
y: cardY,
|
||||
width: DEFAULT_CARD_W,
|
||||
height: DEFAULT_CARD_H,
|
||||
expandedSessionIds,
|
||||
}));
|
||||
}
|
||||
dispatch(setPendingFocusAgentId(sid));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{card.view === 'history_detail' && workflow && (
|
||||
@@ -725,21 +688,6 @@ const WorkflowCard: React.FC<Props> = ({
|
||||
message={runToast || ''}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
/>
|
||||
{/* Ghost-protection dialog: only opens when an enabled-schedule
|
||||
card is X'd out. Cancel keeps the card; "Hide card" closes
|
||||
but leaves the schedule alive; "Stop & delete" wipes the
|
||||
workflow entirely. */}
|
||||
<Dialog open={closeConfirmOpen} onClose={() => setCloseConfirmOpen(false)}>
|
||||
<DialogTitle>Close this workflow card?</DialogTitle>
|
||||
<DialogContent>
|
||||
The schedule will keep firing in the background even after you close this card. Choose what you want to happen.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCloseConfirmOpen(false)}>Cancel</Button>
|
||||
<Button onClick={onConfirmHide}>Hide card (schedule keeps running)</Button>
|
||||
<Button color="error" onClick={onConfirmStopAndDelete}>Stop & delete</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,8 +26,6 @@ export default function WorkflowEditViews({ workflow, facet, onChangeFacet, onDi
|
||||
const dispatch = useAppDispatch();
|
||||
const [draft, setDraft] = useState<Workflow>(workflow);
|
||||
const [busy, setBusy] = useState(false);
|
||||
// Save-feedback state. `savedFlash` flashes a checkmark for 1.4s then
|
||||
// auto-clears; `saveError` carries a string the user can read.
|
||||
const [savedFlash, setSavedFlash] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
@@ -39,20 +37,9 @@ export default function WorkflowEditViews({ workflow, facet, onChangeFacet, onDi
|
||||
// a stale "you have unsaved changes" dot on the tab.
|
||||
useEffect(() => () => { onDirtyChange?.(false); }, [onDirtyChange]);
|
||||
|
||||
// Auto-save on a quiet idle. We debounce 800ms after the last edit so
|
||||
// rapid typing doesn't fire dozens of PATCHes. Validation still gates
|
||||
// the network call so bad drafts (empty phone, etc.) don't auto-save
|
||||
// a broken state. Explicit Save still works for users who want it.
|
||||
useEffect(() => {
|
||||
if (!dirty || busy) return;
|
||||
if (validateDraft(draft)) return; // skip auto-save while invalid
|
||||
const handle = window.setTimeout(() => { onSaveRef.current?.(); }, 800);
|
||||
return () => window.clearTimeout(handle);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [draft, dirty, busy]);
|
||||
// onSave refs itself so the effect above doesn't depend on it.
|
||||
const onSaveRef = React.useRef<(() => Promise<void>) | null>(null);
|
||||
|
||||
// Save is explicit only. The previous auto-save raced the Save button:
|
||||
// the user toggled a field, autosave fired 800ms later, dirty went
|
||||
// false, and a manual Save click became a no-op.
|
||||
const onSave = useCallback(async () => {
|
||||
if (busy || !dirty) return;
|
||||
const reason = validateDraft(draft);
|
||||
@@ -72,6 +59,11 @@ export default function WorkflowEditViews({ workflow, facet, onChangeFacet, onDi
|
||||
ifMatch: workflow.updated_at || null,
|
||||
}));
|
||||
if (updateWorkflow.fulfilled.match(result)) {
|
||||
// Rebase the draft on the server's echoed copy. Without this,
|
||||
// `dirty` would stay true after Save (because updated_at differs)
|
||||
// and the user would see a phantom "unsaved" state.
|
||||
const saved = result.payload as Workflow;
|
||||
if (saved) setDraft(saved);
|
||||
setSavedFlash(true);
|
||||
setTimeout(() => setSavedFlash(false), 1400);
|
||||
} else if (result.payload?.kind === 'stale') {
|
||||
@@ -86,10 +78,6 @@ export default function WorkflowEditViews({ workflow, facet, onChangeFacet, onDi
|
||||
}
|
||||
}, [busy, dirty, dispatch, workflow.id, workflow.updated_at, draft]);
|
||||
|
||||
// Keep the ref pointing at the latest onSave so the auto-save effect
|
||||
// can call it without re-subscribing on every keystroke.
|
||||
useEffect(() => { onSaveRef.current = onSave; }, [onSave]);
|
||||
|
||||
const onDiscard = useCallback(() => {
|
||||
setDraft(workflow);
|
||||
setSaveError(null);
|
||||
@@ -109,32 +97,36 @@ export default function WorkflowEditViews({ workflow, facet, onChangeFacet, onDi
|
||||
Discard + Save are the same pill-style buttons used at the
|
||||
bottom of SavedView; placing them here gives the user a single
|
||||
place to commit OR throw away whatever they just edited. */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
<Typography sx={{ fontSize: LABEL_FS, color: c.text.secondary, fontWeight: 500 }}>Currently Editing</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'nowrap', minWidth: 0 }}>
|
||||
<Typography sx={{ fontSize: LABEL_FS, color: c.text.secondary, fontWeight: 500, flexShrink: 0 }}>Currently Editing</Typography>
|
||||
<Select
|
||||
size="small"
|
||||
value={facet}
|
||||
onChange={(e) => onChangeFacet(e.target.value as Props['facet'])}
|
||||
sx={{ fontSize: LABEL_FS, '& .MuiSelect-select': { py: 0.4 } }}>
|
||||
sx={{ fontSize: LABEL_FS, minWidth: 0, '& .MuiSelect-select': { py: 0.4 } }}>
|
||||
<MenuItem value="General">General</MenuItem>
|
||||
<MenuItem value="Actions">Actions</MenuItem>
|
||||
<MenuItem value="Schedule">Schedule</MenuItem>
|
||||
</Select>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<ActionBtn
|
||||
label="Discard"
|
||||
tone="danger"
|
||||
icon="trash"
|
||||
disabled={!dirty || busy}
|
||||
onClick={onDiscard}
|
||||
/>
|
||||
<ActionBtn
|
||||
label={busy ? 'Saving…' : 'Save'}
|
||||
tone="success"
|
||||
icon="check"
|
||||
disabled={!dirty || busy || saveState === 'saved'}
|
||||
onClick={onSave}
|
||||
/>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }} />
|
||||
<Box sx={{ display: 'inline-flex', flexShrink: 0 }}>
|
||||
<ActionBtn
|
||||
label="Discard"
|
||||
tone="danger"
|
||||
icon="trash"
|
||||
disabled={!dirty || busy}
|
||||
onClick={onDiscard}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'inline-flex', flexShrink: 0, minWidth: 80, justifyContent: 'center' }}>
|
||||
<ActionBtn
|
||||
label={busy ? 'Saving…' : 'Save'}
|
||||
tone="success"
|
||||
icon="check"
|
||||
disabled={!dirty || busy || saveState === 'saved'}
|
||||
onClick={onSave}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{saveError && (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
export const BODY_FS = '0.88rem';
|
||||
@@ -42,8 +44,8 @@ export function ActionBtn({ label, tone, disabled, onClick, icon }: { label: str
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
'&:hover': { bgcolor: palette.hover },
|
||||
}}>
|
||||
{icon === 'trash' && <Box component="span" sx={{ fontSize: 13, lineHeight: 1 }}>{'\u{1F5D1}'}</Box>}
|
||||
{icon === 'check' && <Box component="span" sx={{ fontSize: 13, lineHeight: 1 }}>{'✓'}</Box>}
|
||||
{icon === 'trash' && <DeleteOutlineIcon sx={{ fontSize: 15 }} />}
|
||||
{icon === 'check' && <CheckIcon sx={{ fontSize: 15 }} />}
|
||||
{label}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1000,6 +1000,8 @@ const agentsSlice = createSlice({
|
||||
? existing.pending_approvals
|
||||
: s.pending_approvals ?? [],
|
||||
tool_group_meta: { ...existing?.tool_group_meta, ...s.tool_group_meta },
|
||||
mcp_suggestions: existing?.mcp_suggestions ?? [],
|
||||
mcp_suggestions_is_vague: existing?.mcp_suggestions_is_vague ?? false,
|
||||
};
|
||||
if (activeStatuses.has(s.status) && !state.trackedNotificationIds.includes(s.id)) {
|
||||
state.trackedNotificationIds.push(s.id);
|
||||
@@ -1181,6 +1183,12 @@ const agentsSlice = createSlice({
|
||||
...session,
|
||||
pending_approvals: session.pending_approvals ?? existing?.pending_approvals ?? [],
|
||||
tool_group_meta: session.tool_group_meta ?? existing?.tool_group_meta ?? {},
|
||||
// mcp_suggestions live in client state only (the backend never
|
||||
// returns them in the session payload). Preserve them across
|
||||
// refresh so the suggestion banner stays put until the user
|
||||
// dismisses it or activates one.
|
||||
mcp_suggestions: existing?.mcp_suggestions ?? [],
|
||||
mcp_suggestions_is_vague: existing?.mcp_suggestions_is_vague ?? false,
|
||||
};
|
||||
})
|
||||
.addCase(fetchSession.rejected, (state, action) => {
|
||||
|
||||
@@ -630,7 +630,12 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.pendingFocusWorkflowId = workflowId;
|
||||
return;
|
||||
}
|
||||
const rects = collectOccupiedRects(state, expandedSessionIds);
|
||||
// Fall back to persistedExpandedSessionIds when the caller didn't
|
||||
// wire the live list through. Without it, collectOccupiedRects sees
|
||||
// every chat at its stored (collapsed) height, and a workflow
|
||||
// spawned from an open chat lands on top of the visibly-tall card.
|
||||
const expanded = expandedSessionIds ?? state.persistedExpandedSessionIds;
|
||||
const rects = collectOccupiedRects(state, expanded);
|
||||
let posX: number, posY: number;
|
||||
const parentCard = sourceSessionId ? state.cards[sourceSessionId] : null;
|
||||
if (parentCard) {
|
||||
|
||||
Reference in New Issue
Block a user