mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 04:24:51 +02:00
[aidan] feat/run-context: attach a run as removable chat context
This commit is contained in:
@@ -64,6 +64,7 @@ import ContextDrawer from './shell/ContextDrawer';
|
||||
import { ErrorSlime } from '@/app/components/feedback/ErrorSlime';
|
||||
import { ContextPath } from '@/app/components/editor/DirectoryBrowser';
|
||||
import { setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards, removeCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { WorkflowsRunContext } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { setCardSidecar, commitDraft, updateWorkflowCard, controlWorkflowRun } from '@/shared/state/workflowsSlice';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
@@ -238,6 +239,7 @@ interface QueuedMessage {
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>;
|
||||
selectedBrowserIds?: string[];
|
||||
selectedAppIds?: string[];
|
||||
attachedRunId?: string;
|
||||
}
|
||||
|
||||
interface AgentChatProps {
|
||||
@@ -252,9 +254,20 @@ interface AgentChatProps {
|
||||
// Set when this chat is the workflow build/edit agent: the out-of-tokens card
|
||||
// then warns that switching models here also changes the workflow's run model.
|
||||
workflowEditId?: string;
|
||||
// View-only transcript (e.g. the Run Monitor): renders messages + tool calls
|
||||
// but no composer, so the session can't be typed into.
|
||||
readOnly?: boolean;
|
||||
// One-shot text to drop into the composer (e.g. a run attached as context).
|
||||
prefillPrompt?: string;
|
||||
// A workflow run attached as a removable context chip above the composer; while
|
||||
// present, each send routes through onSendRunQuestion so the run's transcript
|
||||
// rides along as hidden context for that turn.
|
||||
runContext?: WorkflowsRunContext;
|
||||
onClearRunContext?: () => void;
|
||||
onSendRunQuestion?: (prompt: string, runId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose, embedded, autoFocus, initialContextPaths, onBranch, workflowEditId }) => {
|
||||
const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose, embedded, autoFocus, initialContextPaths, onBranch, workflowEditId, readOnly, prefillPrompt, runContext, onClearRunContext, onSendRunQuestion }) => {
|
||||
const c = useClaudeTokens();
|
||||
const STATUS_STYLES: Record<string, { color: string; bg: string }> = {
|
||||
running: { color: c.status.success, bg: c.status.successBg },
|
||||
@@ -362,6 +375,13 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
const [workflowModelNotice, setWorkflowModelNotice] = useState<string | null>(null);
|
||||
const workflowModelNoticeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Read live in the stable handleSend/dispatchMessage closures without busting
|
||||
// their memo (ChatInput leans on handleSend identity holding across renders).
|
||||
const runContextRef = useRef(runContext);
|
||||
runContextRef.current = runContext;
|
||||
const onSendRunQuestionRef = useRef(onSendRunQuestion);
|
||||
onSendRunQuestionRef.current = onSendRunQuestion;
|
||||
|
||||
const wsRef = useRef<ReturnType<typeof createSessionWs> | null>(null);
|
||||
// Current status for the WS-cleanup closure (effect deps can't include it).
|
||||
const statusRef = useRef<string | undefined>(undefined);
|
||||
@@ -470,6 +490,10 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (msg.attachedRunId && onSendRunQuestionRef.current) {
|
||||
// Run-context question: the backend folds the run transcript into this one
|
||||
// turn and echoes the user bubble + answer over WS, so no optimistic thunk.
|
||||
onSendRunQuestionRef.current(msg.prompt, msg.attachedRunId).catch(() => setAwaitingResponse(false));
|
||||
} else {
|
||||
if (msg.selectedBrowserIds?.length) {
|
||||
dispatch(setGlowingBrowserCards({ browserIds: msg.selectedBrowserIds, sessionId: id, label: 'Use Browser' }));
|
||||
@@ -906,7 +930,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
) => {
|
||||
if (!id) return;
|
||||
scrollToBottom();
|
||||
const msg: QueuedMessage = { prompt, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds, selectedAppIds };
|
||||
const msg: QueuedMessage = { prompt, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds, selectedAppIds, attachedRunId: runContextRef.current?.runId };
|
||||
if (agentBusy) {
|
||||
messageQueueRef.current.push(msg);
|
||||
setQueueLength(messageQueueRef.current.length);
|
||||
@@ -1819,7 +1843,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{showResumeBubble && session.status === 'stopped' && !isWorkflowRunSidecar && (
|
||||
{showResumeBubble && session.status === 'stopped' && !isWorkflowRunSidecar && !readOnly && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', my: 0.75 }}>
|
||||
<Box
|
||||
onClick={handleResume}
|
||||
@@ -2255,7 +2279,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
</Fade>
|
||||
);
|
||||
})()}
|
||||
{isStoppableSidecar ? (
|
||||
{readOnly ? null : isStoppableSidecar ? (
|
||||
<ForceStopAgentBar onStop={handleStop} onSaveWorkflow={onTestSaveWorkflow} onContinueEditing={onTestContinueEditing} testState={testState} />
|
||||
) : (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
@@ -2274,6 +2298,10 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
contextEstimate={contextEstimate}
|
||||
sessionId={id}
|
||||
autoFocus={autoFocus}
|
||||
prefillPrompt={prefillPrompt}
|
||||
placeholderOverride={runContext ? 'Ask about this run...' : undefined}
|
||||
runContext={runContext}
|
||||
onClearRunContext={onClearRunContext}
|
||||
thinkingLevel={session?.thinking_level ?? 'auto'}
|
||||
onThinkingLevelChange={handleThinkingLevelChange}
|
||||
onActivityLabelChange={setPreSendActivityLabel}
|
||||
@@ -2306,7 +2334,7 @@ function WorkflowModelNotice({ c, label }: { c: ReturnType<typeof useClaudeToken
|
||||
}}>
|
||||
<SwapHorizRoundedIcon sx={{ fontSize: 17, color: c.accent.primary, flexShrink: 0 }} />
|
||||
<Box sx={{ fontSize: '0.83rem', color: c.text.primary, lineHeight: 1.4 }}>
|
||||
This workflow will run on <b>{display}</b> after you save.
|
||||
This workflow will now be using <b>{display}</b>.
|
||||
</Box>
|
||||
</Box>
|
||||
</Fade>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ChatInputView } from './ChatInput/view/ChatInputView';
|
||||
import { PastePreviewDialog } from './ChatInput/view/PastePreviewDialog';
|
||||
import { ICON_MAP, FALLBACK_MODE_BASE } from './ChatInput/modeConfig';
|
||||
import { AttachedImage, ForcedToolGroup, ChatInputHandle } from './ChatInput/types';
|
||||
import type { WorkflowsRunContext } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
export type { AttachedImage, ForcedToolGroup, ChatInputHandle };
|
||||
export type { AttachedSkill } from '@/app/components/editor/richEditorUtils';
|
||||
@@ -46,9 +47,14 @@ interface Props {
|
||||
// Seed the composer with this text (unsent), so a starter-prompt click opens
|
||||
// the chat with the message already typed, ready for the user to hit send.
|
||||
prefillPrompt?: string;
|
||||
// Replaces the default "Agent, @ for context..." placeholder (e.g. "Ask about this run...").
|
||||
placeholderOverride?: string;
|
||||
// A workflow run shown as a small removable chip inside the composer.
|
||||
runContext?: WorkflowsRunContext;
|
||||
onClearRunContext?: () => void;
|
||||
}
|
||||
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange, prefillPrompt }, ref) => {
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange, prefillPrompt, placeholderOverride, runContext, onClearRunContext }, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -349,6 +355,9 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
isRunning={isRunning}
|
||||
queueLength={queueLength}
|
||||
modeConf={modeConf}
|
||||
placeholderOverride={placeholderOverride}
|
||||
runContext={runContext}
|
||||
onClearRunContext={onClearRunContext}
|
||||
handleInput={handleInput}
|
||||
handleEditorClick={handleEditorClick}
|
||||
handleKeyDown={handleKeyDown}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ContextPath } from '@/app/components/editor/DirectoryBrowser';
|
||||
import { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import { TriggerState } from '@/app/components/editor/richEditorUtils';
|
||||
import { AttachedImage, ForcedToolGroup } from '../types';
|
||||
import type { WorkflowsRunContext } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { SendBlock } from '../hooks/useContextFiles';
|
||||
import { ModelPickerState } from '../hooks/useModelPicker';
|
||||
import { SendBlockBanner } from './SendBlockBanner';
|
||||
@@ -59,6 +60,9 @@ interface Props {
|
||||
isRunning?: boolean;
|
||||
queueLength: number;
|
||||
modeConf: ModeConf;
|
||||
placeholderOverride?: string;
|
||||
runContext?: WorkflowsRunContext;
|
||||
onClearRunContext?: () => void;
|
||||
handleInput: () => void;
|
||||
handleEditorClick: () => void;
|
||||
handleKeyDown: (e: React.KeyboardEvent) => void;
|
||||
@@ -180,6 +184,32 @@ export const ChatInputView: React.FC<Props> = (p) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{p.runContext && (
|
||||
<Box sx={{ display: 'flex', mt: 1, mx: 1.5 }}>
|
||||
<Box
|
||||
title={p.runContext.metaLabel}
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.625, maxWidth: '100%',
|
||||
pl: 0.875, pr: 0.5, py: 0.25, borderRadius: '999px',
|
||||
bgcolor: c.bg.secondary, border: `1px solid ${c.border.subtle}`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: p.runContext.color, flex: 'none' }} />
|
||||
<Typography sx={{ fontSize: '0.7rem', fontWeight: 600, color: c.text.secondary, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
Run attached · {p.runContext.title}
|
||||
</Typography>
|
||||
<Box
|
||||
role="button"
|
||||
aria-label="Remove run context"
|
||||
onClick={p.onClearRunContext}
|
||||
sx={{ width: 15, height: 15, flex: 'none', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: c.text.tertiary, '&:hover': { color: c.text.secondary } }}
|
||||
>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M18 6 6 18M6 6l12 12" /></svg>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<AttachmentChips
|
||||
c={c}
|
||||
images={p.images}
|
||||
@@ -207,7 +237,7 @@ export const ChatInputView: React.FC<Props> = (p) => {
|
||||
autoRunMode={p.autoRunMode}
|
||||
isRunning={p.isRunning}
|
||||
queueLength={p.queueLength}
|
||||
placeholderLabel={`${p.modeConf.label}, @ for context, / for commands`}
|
||||
placeholderLabel={p.placeholderOverride ?? `${p.modeConf.label}, @ for context, / for commands`}
|
||||
onInput={p.handleInput}
|
||||
onClick={p.handleEditorClick}
|
||||
onKeyDown={p.handleKeyDown}
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { runWorkflowNow } from '@/shared/state/workflowsSlice';
|
||||
import { openWorkflowMonitor, setWorkflowsRunContext, clearWorkflowsRunContext } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { stepsSignature, isScheduleActive } from '@/app/pages/Workflows/scheduleUtils';
|
||||
import { askRun } from './api';
|
||||
import AgentChat from '@/app/pages/AgentChat/AgentChat';
|
||||
import { WC, colorForId, statusChip } from './uiKit';
|
||||
import { isRunning } from './model';
|
||||
import { useWC, colorForWorkflow, statusChip } from './uiKit';
|
||||
import { isRunning, runContextChip } from './model';
|
||||
import { useEditAgentSession } from './useEditAgentSession';
|
||||
import { useWorkflowPatch } from './useWorkflowPatch';
|
||||
import ScheduleCard from './ScheduleCard';
|
||||
import StepsCard from './StepsCard';
|
||||
import HistoryCard from './HistoryCard';
|
||||
import ColorSwatch from './ColorSwatch';
|
||||
import type { AppNav } from './types';
|
||||
|
||||
const DetailView: React.FC<{ workflowId: string; nav: AppNav }> = ({ workflowId }) => {
|
||||
const WC = useWC();
|
||||
const dispatch = useAppDispatch();
|
||||
const patch = useWorkflowPatch();
|
||||
const workflow = useAppSelector((s) => s.workflows.items[workflowId]);
|
||||
const active = useAppSelector((s) => s.workflows.active);
|
||||
const sessionId = useEditAgentSession(workflowId, 'modify');
|
||||
const sessionId = useEditAgentSession(workflowId);
|
||||
const [name, setName] = useState(workflow?.title ?? '');
|
||||
const detailRuns = useAppSelector((s) => s.workflows.runs[workflowId]);
|
||||
const runContext = useAppSelector((s) => s.dashboardLayout.workflowsRunContext);
|
||||
// When you Run now from this chat, attach that run as a context chip once it
|
||||
// finishes, so the next question rides on its transcript (removable, no popup).
|
||||
const autoCtxRunId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => { setName(workflow?.title ?? ''); }, [workflow?.title]);
|
||||
useEffect(() => {
|
||||
const rid = autoCtxRunId.current;
|
||||
if (!rid) return;
|
||||
const r = (detailRuns || []).find((x) => x.id === rid);
|
||||
if (!r || r.status === 'running') return;
|
||||
autoCtxRunId.current = null;
|
||||
if (workflow) dispatch(setWorkflowsRunContext(runContextChip(workflow, r)));
|
||||
}, [detailRuns, workflowId, dispatch, workflow]);
|
||||
|
||||
if (!workflow) return <div style={{ flex: 1, background: WC.paper }} />;
|
||||
|
||||
@@ -31,7 +48,9 @@ const DetailView: React.FC<{ workflowId: string; nav: AppNav }> = ({ workflowId
|
||||
|
||||
const runNow = () => {
|
||||
if (running) return;
|
||||
dispatch(runWorkflowNow({ id: workflow.id, signature: stepsSignature(workflow.steps) }));
|
||||
dispatch(runWorkflowNow({ id: workflow.id, signature: stepsSignature(workflow.steps) }))
|
||||
.unwrap().then((res) => { autoCtxRunId.current = res.run_id || null; }).catch(() => {});
|
||||
dispatch(openWorkflowMonitor({ workflowId: workflow.id }));
|
||||
};
|
||||
const commitName = () => {
|
||||
const t = name.trim();
|
||||
@@ -41,9 +60,9 @@ const DetailView: React.FC<{ workflowId: string; nav: AppNav }> = ({ workflowId
|
||||
return (
|
||||
<>
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', background: WC.paper }}>
|
||||
<div style={{ flex: 'none', padding: '20px 28px 16px', borderBottom: '1px solid rgba(33,30,27,0.06)' }}>
|
||||
<div style={{ flex: 'none', padding: '20px 28px 16px', borderBottom: `1px solid rgba(${WC.inkRGB},0.06)` }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 13 }}>
|
||||
<div style={{ width: 14, height: 14, borderRadius: 4, background: colorForId(workflow.id), boxShadow: '0 0 0 1px rgba(33,30,27,0.14)', flex: 'none' }} />
|
||||
<ColorSwatch value={colorForWorkflow(workflow)} onChange={(hex) => patch(workflow, { color: hex })} size={14} />
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
@@ -51,12 +70,12 @@ const DetailView: React.FC<{ workflowId: string; nav: AppNav }> = ({ workflowId
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }}
|
||||
style={{ flex: 1, minWidth: 0, border: 'none', background: 'transparent', fontFamily: "'Newsreader',serif", fontSize: 25, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em' }}
|
||||
/>
|
||||
<span style={statusChip(status)}>{statusText}</span>
|
||||
<span style={statusChip(status, WC)}>{statusText}</span>
|
||||
<button onClick={runNow} disabled={running} style={{ display: 'flex', alignItems: 'center', gap: 8, background: running ? WC.inset : WC.ink, color: running ? WC.muted : WC.paper, border: 'none', borderRadius: 9, padding: '8px 15px', fontSize: 13, fontWeight: 600, cursor: running ? 'default' : 'pointer', flex: 'none' }}>
|
||||
{running
|
||||
? <div style={{ width: 12, height: 12, borderRadius: '50%', border: '2px solid rgba(140,133,122,0.3)', borderTopColor: WC.muted, animation: 'os-spin 0.7s linear infinite', flex: 'none' }} />
|
||||
: <div style={{ width: 0, height: 0, borderTop: '5px solid transparent', borderBottom: '5px solid transparent', borderLeft: `8px solid ${WC.paper}`, flex: 'none' }} />}
|
||||
<span>{running ? 'Running…' : 'Run now'}</span>
|
||||
<span>{running ? 'Running…' : 'Run'}</span>
|
||||
</button>
|
||||
</div>
|
||||
{workflow.description && <div style={{ fontSize: 13.5, color: WC.muted, marginTop: 7, paddingLeft: 27 }}>{workflow.description}</div>}
|
||||
@@ -64,7 +83,14 @@ const DetailView: React.FC<{ workflowId: string; nav: AppNav }> = ({ workflowId
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{sessionId
|
||||
? <AgentChat sessionId={sessionId} embedded workflowEditId={workflow.id} />
|
||||
? <AgentChat
|
||||
sessionId={sessionId}
|
||||
embedded
|
||||
workflowEditId={workflow.id}
|
||||
runContext={runContext?.workflowId === workflow.id ? runContext : undefined}
|
||||
onClearRunContext={() => dispatch(clearWorkflowsRunContext())}
|
||||
onSendRunQuestion={(prompt, runId) => askRun(workflow.id, { runId, prompt }).then((ok) => { if (!ok) throw new Error('ask-run failed'); })}
|
||||
/>
|
||||
: <div style={{ flex: 1 }} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,3 +23,22 @@ export async function ensureEditAgentSession(workflowId: string): Promise<string
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Send a chat question with a run's transcript riding along as hidden context
|
||||
// for that single turn, so the answer is grounded in the run without an extra
|
||||
// "I've reviewed it" round-trip. The user's bubble shows just their question.
|
||||
export async function askRun(
|
||||
workflowId: string,
|
||||
body: { runId: string; prompt: string; mode?: string; model?: string },
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${base}/${encodeURIComponent(workflowId)}/ask-run`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ run_id: body.runId, prompt: body.prompt, mode: body.mode, model: body.model }),
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Workflow, WorkflowRun, ScheduleConfig, ActiveRun } from '@/shared/state/workflowsSlice';
|
||||
import type { WorkflowsRunContext } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { isScheduleActive, fireTimesWithin } from '@/app/pages/Workflows/scheduleUtils';
|
||||
|
||||
// The design speaks in four cadence buckets; the backend speaks in repeat_unit.
|
||||
@@ -129,6 +130,31 @@ export function runDuration(run: WorkflowRun): string {
|
||||
return `${Math.floor(s / 60)}m ${s % 60}s`;
|
||||
}
|
||||
|
||||
export function runContextChip(workflow: Workflow, run: WorkflowRun): WorkflowsRunContext {
|
||||
const total = workflow.steps.length;
|
||||
const aidx = run.active_step_idx ?? 0;
|
||||
const dur = runDuration(run);
|
||||
const statusWord = run.status === 'success' ? 'completed'
|
||||
: run.status === 'ran_late' ? 'completed late'
|
||||
: run.status === 'failure' ? 'failed'
|
||||
: run.status === 'running' ? 'running'
|
||||
: run.status === 'skipped' ? 'skipped' : run.status;
|
||||
const stepsPart = run.status === 'failure' ? `failed at step ${Math.min(aidx + 1, total)}`
|
||||
: (run.status === 'success' || run.status === 'ran_late') ? `${total}/${total} steps`
|
||||
: total > 0 ? `${Math.min(aidx, total)}/${total} steps` : '';
|
||||
const title = run.triggered_by === 'manual' ? 'Manual run'
|
||||
: run.triggered_by === 'retry' ? 'Re-run' : 'Scheduled run';
|
||||
const color = run.status === 'failure' ? '#C2483A'
|
||||
: (run.status === 'success' || run.status === 'ran_late') ? '#3F8E5B' : '#C25A36';
|
||||
return {
|
||||
workflowId: workflow.id,
|
||||
runId: run.id,
|
||||
title,
|
||||
metaLabel: [statusWord, dur, stepsPart].filter(Boolean).join(' · '),
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
export function toRunRow(run: WorkflowRun, title: string): RunRow {
|
||||
return {
|
||||
id: run.id,
|
||||
|
||||
@@ -164,6 +164,16 @@ export interface DashboardLayoutState {
|
||||
workflowsMonitorRunId: string | null;
|
||||
/** Geometry of the spawned Run Monitor card (a real canvas card, tethered to the window). Ephemeral, not persisted. */
|
||||
workflowsMonitorCard: WorkflowsHubPosition | null;
|
||||
/** A run attached to a workflow's chat as a removable context chip; its transcript rides along each send until removed. */
|
||||
workflowsRunContext: WorkflowsRunContext | null;
|
||||
}
|
||||
|
||||
export interface WorkflowsRunContext {
|
||||
workflowId: string;
|
||||
runId: string;
|
||||
title: string;
|
||||
metaLabel: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const initialState: DashboardLayoutState = {
|
||||
@@ -194,6 +204,7 @@ const initialState: DashboardLayoutState = {
|
||||
workflowsMonitorId: null,
|
||||
workflowsMonitorRunId: null,
|
||||
workflowsMonitorCard: null,
|
||||
workflowsRunContext: null,
|
||||
};
|
||||
|
||||
interface LayoutPayload {
|
||||
@@ -1063,6 +1074,14 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.workflowsMonitorCard.y = action.payload.y;
|
||||
},
|
||||
|
||||
setWorkflowsRunContext(state, action: PayloadAction<WorkflowsRunContext>) {
|
||||
state.workflowsRunContext = action.payload;
|
||||
},
|
||||
|
||||
clearWorkflowsRunContext(state) {
|
||||
state.workflowsRunContext = null;
|
||||
},
|
||||
|
||||
setWorkflowsHubPosition(state, action: PayloadAction<{ x: number; y: number }>) {
|
||||
if (!state.workflowsHub) return;
|
||||
state.workflowsHub.x = action.payload.x;
|
||||
@@ -1571,6 +1590,8 @@ export const {
|
||||
openWorkflowMonitor,
|
||||
closeWorkflowMonitor,
|
||||
setWorkflowsMonitorPosition,
|
||||
setWorkflowsRunContext,
|
||||
clearWorkflowsRunContext,
|
||||
setWorkflowsHubPosition,
|
||||
setWorkflowsHubSize,
|
||||
clearPendingFocusWorkflowsHub,
|
||||
|
||||
Reference in New Issue
Block a user