feat(intelligence): workflow orchestration UI + approval card (#3375) (#3697)

This commit is contained in:
oxoxDev
2026-06-15 18:29:12 -07:00
committed by GitHub
parent 976b31f631
commit ae24451b2a
24 changed files with 2632 additions and 0 deletions
@@ -0,0 +1,345 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
type WorkflowDefinition,
type WorkflowRun,
workflowRunsApi,
} from '../../services/api/workflowRunsApi';
import IntelligenceOrchestrationTab from './IntelligenceOrchestrationTab';
vi.mock('../../services/api/workflowRunsApi', async importOriginal => {
// Keep the real assessWorkflowCost / thresholds; mock only the RPC client.
const actual = await importOriginal<typeof import('../../services/api/workflowRunsApi')>();
return {
...actual,
workflowRunsApi: {
listDefinitions: vi.fn(),
listRuns: vi.fn(),
getRun: vi.fn(),
startRun: vi.fn(),
stopRun: vi.fn(),
resumeRun: vi.fn(),
},
};
});
// i18n → echo the key so assertions can target stable strings.
vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
const api = vi.mocked(workflowRunsApi);
function builtin(overrides: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
return {
id: 'parallel_research_cross_check',
name: 'Parallel research',
description: 'desc',
phases: [{ name: 'decompose', description: '', agentIds: ['planner'], dependsOn: [] }],
defaultConcurrency: 2,
maxChildren: 8, // >= threshold → approval required
safetyTier: 'read_only',
...overrides,
};
}
function startedRun(): WorkflowRun {
return {
id: 'wfrun-1',
definitionId: 'parallel_research_cross_check',
parentThreadId: null,
input: {},
phaseStates: { decompose: { status: 'running', outputs: [] } },
childRunIds: [],
status: 'running',
summary: null,
startedAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
completedAt: null,
};
}
describe('IntelligenceOrchestrationTab — approval gating', () => {
beforeEach(() => {
api.listDefinitions.mockReset();
api.listRuns.mockReset();
api.startRun.mockReset();
api.getRun.mockReset();
api.listRuns.mockResolvedValue([]);
api.getRun.mockResolvedValue(startedRun());
});
it('shows the approval card (not a direct start) for a high-cost definition', async () => {
api.listDefinitions.mockResolvedValue([builtin()]);
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-start-parallel_research_cross_check'));
expect(screen.getByTestId('workflow-approval-card')).toBeInTheDocument();
// No direct "start run" button when approval is required.
expect(screen.queryByTestId('orchestration-confirm-start')).not.toBeInTheDocument();
// startRun must NOT have fired yet — approval is still pending.
expect(api.startRun).not.toHaveBeenCalled();
});
it('starts the run only after the approval is granted', async () => {
api.listDefinitions.mockResolvedValue([builtin()]);
api.startRun.mockResolvedValue(startedRun());
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-start-parallel_research_cross_check'));
fireEvent.click(screen.getByTestId('workflow-approval-approve'));
await waitFor(() =>
expect(api.startRun).toHaveBeenCalledWith({
definitionId: 'parallel_research_cross_check',
input: undefined,
})
);
});
it('starts directly (no approval card) for a cheap read-only definition', async () => {
api.listDefinitions.mockResolvedValue([
builtin({ id: 'cheap', name: 'Cheap', maxChildren: 3, defaultConcurrency: 2 }),
]);
api.startRun.mockResolvedValue(startedRun());
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-start-cheap'));
expect(screen.queryByTestId('workflow-approval-card')).not.toBeInTheDocument();
expect(screen.getByTestId('orchestration-confirm-start')).toBeInTheDocument();
});
});
const cheap = () => builtin({ id: 'cheap', name: 'Cheap', maxChildren: 3, defaultConcurrency: 2 });
describe('IntelligenceOrchestrationTab — load + empty states', () => {
beforeEach(() => {
api.listDefinitions.mockReset();
api.listRuns.mockReset();
api.startRun.mockReset();
api.getRun.mockReset();
api.stopRun.mockReset();
api.resumeRun.mockReset();
});
it('shows the error state when loading fails and retries on click', async () => {
api.listDefinitions.mockRejectedValueOnce(new Error('rpc boom'));
api.listRuns.mockResolvedValue([]);
render(<IntelligenceOrchestrationTab />);
// load() catch path (lines 79-81) + error render (224) + retry button (231-233).
expect(await screen.findByText(/orchestration.failedToLoad/)).toBeInTheDocument();
expect(screen.getByText(/rpc boom/)).toBeInTheDocument();
// Second attempt succeeds and clears the error.
api.listDefinitions.mockResolvedValueOnce([cheap()]);
fireEvent.click(screen.getByText('common.retry'));
expect(await screen.findByTestId('orchestration-start-cheap')).toBeInTheDocument();
});
it('renders the empty definitions + empty runs placeholders', async () => {
api.listDefinitions.mockResolvedValue([]);
api.listRuns.mockResolvedValue([]);
render(<IntelligenceOrchestrationTab />);
// noDefinitions (251) + noRuns placeholders.
expect(await screen.findByText('orchestration.noDefinitions')).toBeInTheDocument();
expect(screen.getByText('orchestration.noRuns')).toBeInTheDocument();
});
});
describe('IntelligenceOrchestrationTab — start flow + run list', () => {
beforeEach(() => {
api.listDefinitions.mockReset();
api.listRuns.mockReset();
api.startRun.mockReset();
api.getRun.mockReset();
api.stopRun.mockReset();
api.resumeRun.mockReset();
api.listRuns.mockResolvedValue([]);
api.getRun.mockResolvedValue(startedRun());
});
it('starts a cheap run with the typed question and opens its detail view', async () => {
api.listDefinitions.mockResolvedValue([cheap()]);
api.startRun.mockResolvedValue(startedRun());
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-start-cheap'));
// Type a question (covers the controlled textarea onChange, line 305).
fireEvent.change(screen.getByTestId('orchestration-question'), {
target: { value: ' why is the sky blue? ' },
});
// Direct confirm-start (lines 330, 332).
fireEvent.click(screen.getByTestId('orchestration-confirm-start'));
// doStart trims + forwards the question (line 161) and opens the run detail.
await waitFor(() =>
expect(api.startRun).toHaveBeenCalledWith({
definitionId: 'cheap',
input: { question: 'why is the sky blue?' },
})
);
expect(await screen.findByTestId('orchestration-selected-run')).toBeInTheDocument();
// The started run is upserted into the recent-runs list (lines 402-408).
expect(screen.getByTestId('orchestration-run-wfrun-1')).toBeInTheDocument();
});
it('surfaces a start error inline (doStart catch path)', async () => {
api.listDefinitions.mockResolvedValue([cheap()]);
api.startRun.mockRejectedValueOnce(new Error('start failed'));
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-start-cheap'));
fireEvent.click(screen.getByTestId('orchestration-confirm-start'));
// doStart error branch (lines 169-171) renders the inline error.
expect(await screen.findByText(/start failed/)).toBeInTheDocument();
// The panel stays open (no selected run).
expect(screen.queryByTestId('orchestration-selected-run')).not.toBeInTheDocument();
});
it('cancels the start panel without starting a run', async () => {
api.listDefinitions.mockResolvedValue([cheap()]);
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-start-cheap'));
expect(screen.getByTestId('orchestration-confirm-start')).toBeInTheDocument();
// cancelStart (lines 148-150) closes the panel.
fireEvent.click(screen.getByText('orchestration.approval.cancel'));
await waitFor(() =>
expect(screen.queryByTestId('orchestration-confirm-start')).not.toBeInTheDocument()
);
expect(api.startRun).not.toHaveBeenCalled();
});
it('opens a recent run, updates it via upsert, then closes it', async () => {
api.listDefinitions.mockResolvedValue([cheap()]);
// A pre-existing run in the recent list (drives the runs.map render, 402-408).
api.listRuns.mockResolvedValue([startedRun()]);
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-run-wfrun-1'));
expect(await screen.findByTestId('orchestration-selected-run')).toBeInTheDocument();
// Close the drill-in (lines 364-366).
fireEvent.click(screen.getByTestId('orchestration-close-run'));
await waitFor(() =>
expect(screen.queryByTestId('orchestration-selected-run')).not.toBeInTheDocument()
);
});
});
describe('IntelligenceOrchestrationTab — stop / resume controls', () => {
beforeEach(() => {
api.listDefinitions.mockReset();
api.listRuns.mockReset();
api.getRun.mockReset();
api.stopRun.mockReset();
api.resumeRun.mockReset();
api.listDefinitions.mockResolvedValue([cheap()]);
});
it('stops a running run and reflects the interrupted snapshot', async () => {
api.listRuns.mockResolvedValue([startedRun()]);
// First poll snapshot is still running (Stop renders); once stopped the
// backend reports interrupted, so subsequent polls agree and don't race the
// stop result back to running (matches the serialized poll loop).
api.getRun
.mockResolvedValueOnce(startedRun())
.mockResolvedValue({ ...startedRun(), status: 'interrupted' });
api.stopRun.mockResolvedValue({ ...startedRun(), status: 'interrupted' });
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-run-wfrun-1'));
fireEvent.click(await screen.findByTestId('workflow-run-stop'));
// handleStop (lines 179-185) calls stopRun and upserts the result.
await waitFor(() => expect(api.stopRun).toHaveBeenCalledWith('wfrun-1'));
await waitFor(() =>
expect(screen.getByTestId('workflow-run-status')).toHaveTextContent(
'orchestration.runStatus.interrupted'
)
);
});
it('resumes an interrupted run', async () => {
const interrupted = { ...startedRun(), status: 'interrupted' as const };
api.listRuns.mockResolvedValue([interrupted]);
api.getRun.mockResolvedValue(interrupted);
api.resumeRun.mockResolvedValue({ ...startedRun(), status: 'running' });
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-run-wfrun-1'));
fireEvent.click(await screen.findByTestId('workflow-run-resume'));
// handleResume (lines 197-203) calls resumeRun and upserts the result.
await waitFor(() => expect(api.resumeRun).toHaveBeenCalledWith('wfrun-1'));
await waitFor(() =>
expect(screen.getByTestId('workflow-run-status')).toHaveTextContent(
'orchestration.runStatus.running'
)
);
});
it('swallows a stop RPC error without crashing', async () => {
api.listRuns.mockResolvedValue([startedRun()]);
api.getRun.mockResolvedValue(startedRun());
api.stopRun.mockRejectedValueOnce(new Error('stop boom'));
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-run-wfrun-1'));
fireEvent.click(await screen.findByTestId('workflow-run-stop'));
// handleStop catch path (lines 188, 190): still mounted, button re-enabled.
await waitFor(() => expect(api.stopRun).toHaveBeenCalled());
expect(screen.getByTestId('orchestration-selected-run')).toBeInTheDocument();
});
});
describe('IntelligenceOrchestrationTab — polling loop', () => {
beforeEach(() => {
api.listDefinitions.mockReset();
api.listRuns.mockReset();
api.getRun.mockReset();
api.listDefinitions.mockResolvedValue([cheap()]);
api.listRuns.mockResolvedValue([startedRun()]);
});
it('polls getRun for a selected non-terminal run and upserts the fresh snapshot', async () => {
// First poll returns a completed snapshot.
api.getRun.mockResolvedValue({
...startedRun(),
status: 'completed',
summary: 'done',
completedAt: '2026-01-01T00:05:00Z',
});
render(<IntelligenceOrchestrationTab />);
// Select the run; the parent then starts the real 2s poll interval. We wait
// (real timers) for the first tick rather than fight fake timers vs. the
// interval registered inside the post-select effect.
fireEvent.click(await screen.findByTestId('orchestration-run-wfrun-1'));
expect(await screen.findByTestId('orchestration-selected-run')).toBeInTheDocument();
// tick() runs after POLL_INTERVAL_MS (lines 117-121); upsert flips status.
await waitFor(() => expect(api.getRun).toHaveBeenCalledWith('wfrun-1'), { timeout: 4000 });
await waitFor(() =>
expect(screen.getByTestId('workflow-run-status')).toHaveTextContent(
'orchestration.runStatus.completed'
)
);
});
it('swallows a poll error (tick catch path)', async () => {
api.getRun.mockRejectedValue(new Error('poll boom'));
render(<IntelligenceOrchestrationTab />);
fireEvent.click(await screen.findByTestId('orchestration-run-wfrun-1'));
expect(await screen.findByTestId('orchestration-selected-run')).toBeInTheDocument();
// tick catch (line 123): poll rejects, the run stays open with no crash.
await waitFor(() => expect(api.getRun).toHaveBeenCalled(), { timeout: 4000 });
expect(screen.getByTestId('orchestration-selected-run')).toBeInTheDocument();
});
});
@@ -0,0 +1,437 @@
/**
* IntelligenceOrchestrationTab (#3375)
* ------------------------------------
*
* The Workflows / multi-agent orchestration surface in the Intelligence command
* center. Lets the user:
* - browse runnable workflow definitions (`workflow_run_list_definitions`),
* - start the parallel-research workflow (`workflow_run_start`) — gated behind
* an explicit approval card for high-cost / high-concurrency definitions,
* - drill into a run's phase progress, child agent refs, and final synthesis,
* - stop / resume a run.
*
* Progress is poll-based (the engine emits no socket events yet): once a run is
* selected and non-terminal, this tab polls `workflow_run_get` on an interval
* and feeds fresh snapshots to {@link WorkflowRunDetail}.
*
* This is a sibling of {@link WorkflowsTab} (which manages SKILL.md authoring) —
* the two are deliberately distinct primitives: that tab is reusable procedures,
* this tab is declarative multi-agent runs.
*/
import debug from 'debug';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useT } from '../../lib/i18n/I18nContext';
import {
assessWorkflowCost,
type WorkflowDefinition,
type WorkflowRun,
workflowRunsApi,
} from '../../services/api/workflowRunsApi';
import { SAFETY_TIER_KEY, WorkflowRunApprovalCard } from './WorkflowRunApprovalCard';
import WorkflowRunDetail from './WorkflowRunDetail';
const log = debug('intelligence:orchestration');
/** How often to poll a selected, non-terminal run for progress. */
const POLL_INTERVAL_MS = 2000;
const TERMINAL = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
function isTerminal(run: WorkflowRun | null): boolean {
return run !== null && TERMINAL.has(run.status);
}
export default function IntelligenceOrchestrationTab() {
const { t } = useT();
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
const [runs, setRuns] = useState<WorkflowRun[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Start flow state.
const [startTarget, setStartTarget] = useState<WorkflowDefinition | null>(null);
const [question, setQuestion] = useState('');
const [starting, setStarting] = useState(false);
const [startError, setStartError] = useState<string | null>(null);
// Drill-in state.
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
const [selectedRun, setSelectedRun] = useState<WorkflowRun | null>(null);
const [controlBusy, setControlBusy] = useState(false);
const mountedRef = useRef(true);
const load = useCallback(async () => {
log('load: entry');
setError(null);
try {
const [defs, runList] = await Promise.all([
workflowRunsApi.listDefinitions(),
workflowRunsApi.listRuns({ limit: 50 }),
]);
if (!mountedRef.current) return;
setDefinitions(defs);
setRuns(runList);
log('load: defs=%d runs=%d', defs.length, runList.length);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
log('load: error %s', msg);
if (mountedRef.current) setError(msg);
} finally {
if (mountedRef.current) setLoading(false);
}
}, []);
useEffect(() => {
mountedRef.current = true;
// Defer the first fetch a tick so the loading state paints before `load`
// touches state (mirrors IntelligenceAgentWorkTab; avoids a synchronous
// setState inside the effect body).
const handle = window.setTimeout(() => void load(), 0);
return () => {
window.clearTimeout(handle);
mountedRef.current = false;
};
}, [load]);
// Merge a fresh run snapshot into the runs list (newest data wins).
const upsertRun = useCallback((run: WorkflowRun) => {
setRuns(prev => {
const idx = prev.findIndex(r => r.id === run.id);
if (idx === -1) return [run, ...prev];
const next = prev.slice();
next[idx] = run;
return next;
});
}, []);
// ---- Polling loop for the selected run -------------------------------
useEffect(() => {
if (!selectedRunId) return;
if (isTerminal(selectedRun)) return; // nothing left to poll
let cancelled = false;
let inFlight = false;
let handle: number | undefined;
const tick = async () => {
if (cancelled || inFlight) return;
inFlight = true;
try {
const run = await workflowRunsApi.getRun(selectedRunId);
if (cancelled || !mountedRef.current || !run) return;
setSelectedRun(run);
upsertRun(run);
if (!isTerminal(run)) {
handle = window.setTimeout(() => void tick(), POLL_INTERVAL_MS);
}
} catch (err) {
log('poll error %s', err instanceof Error ? err.message : String(err));
} finally {
inFlight = false;
}
};
void tick();
return () => {
cancelled = true;
if (handle !== undefined) window.clearTimeout(handle);
};
}, [selectedRunId, selectedRun, upsertRun]);
const openRun = useCallback((run: WorkflowRun) => {
log('openRun id=%s', run.id);
setSelectedRunId(run.id);
setSelectedRun(run);
}, []);
// ---- Start flow ------------------------------------------------------
const beginStart = useCallback((def: WorkflowDefinition) => {
log('beginStart definitionId=%s', def.id);
setStartTarget(def);
setQuestion('');
setStartError(null);
}, []);
const cancelStart = useCallback(() => {
setStartTarget(null);
setStarting(false);
setStartError(null);
}, []);
const doStart = useCallback(async () => {
if (!startTarget) return;
setStarting(true);
setStartError(null);
const trimmed = question.trim();
try {
const run = await workflowRunsApi.startRun({
definitionId: startTarget.id,
input: trimmed.length > 0 ? { question: trimmed } : undefined,
});
if (!mountedRef.current) return;
log('started runId=%s', run.id);
upsertRun(run);
setStartTarget(null);
openRun(run);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
log('start error %s', msg);
if (mountedRef.current) setStartError(msg);
} finally {
if (mountedRef.current) setStarting(false);
}
}, [startTarget, question, upsertRun, openRun]);
// ---- Stop / Resume ---------------------------------------------------
const handleStop = useCallback(
async (id: string) => {
setControlBusy(true);
try {
const run = await workflowRunsApi.stopRun(id);
if (run && mountedRef.current) {
setSelectedRun(run);
upsertRun(run);
}
} catch (err) {
log('stop error %s', err instanceof Error ? err.message : String(err));
} finally {
if (mountedRef.current) setControlBusy(false);
}
},
[upsertRun]
);
const handleResume = useCallback(
async (id: string) => {
setControlBusy(true);
try {
const run = await workflowRunsApi.resumeRun(id);
if (mountedRef.current) {
setSelectedRun(run);
upsertRun(run);
}
} catch (err) {
log('resume error %s', err instanceof Error ? err.message : String(err));
} finally {
if (mountedRef.current) setControlBusy(false);
}
},
[upsertRun]
);
if (loading) {
return (
<div className="flex items-center justify-center py-10 text-stone-400 dark:text-neutral-500">
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-ocean-500 border-t-transparent" />
<span className="text-sm">{t('orchestration.loading')}</span>
</div>
);
}
if (error) {
return (
<div className="space-y-3">
<div className="rounded-xl border border-coral-200 bg-coral-50 px-4 py-3 text-sm text-coral-700 dark:border-coral-500/30 dark:bg-coral-500/10 dark:text-coral-300">
{t('orchestration.failedToLoad')}: {error}
</div>
<button
type="button"
onClick={() => {
setLoading(true);
void load();
}}
className="rounded-lg border border-stone-300 bg-white px-3 py-1.5 text-xs font-medium text-stone-700 hover:bg-stone-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300">
{t('common.retry')}
</button>
</div>
);
}
return (
<div className="space-y-6" data-testid="orchestration-tab">
<p className="text-xs text-stone-400 dark:text-neutral-500">{t('orchestration.subtitle')}</p>
{/* Definitions catalog */}
<section className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wide text-stone-500 dark:text-neutral-400">
{t('orchestration.definitions')}
</h3>
{definitions.length === 0 ? (
<div className="rounded-xl border border-dashed border-stone-200 py-8 text-center text-sm text-stone-400 dark:border-neutral-800 dark:text-neutral-500">
{t('orchestration.noDefinitions')}
</div>
) : (
<ul className="space-y-2" data-testid="orchestration-definitions">
{definitions.map(def => {
const assessment = assessWorkflowCost(def);
const isStarting = startTarget?.id === def.id;
return (
<li
key={def.id}
data-testid={`orchestration-definition-${def.id}`}
className="rounded-xl border border-stone-200 bg-white p-3 dark:border-neutral-800 dark:bg-neutral-900">
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-sm font-medium text-stone-800 dark:text-neutral-100">
{def.name}
</span>
<span className="rounded-md border border-stone-200 px-1.5 py-0.5 text-[10px] font-medium text-stone-500 dark:border-neutral-700 dark:text-neutral-400">
{t(SAFETY_TIER_KEY[def.safetyTier])}
</span>
{assessment.requiresApproval && (
<span
data-testid={`orchestration-approval-badge-${def.id}`}
className="rounded-md border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-300">
{t('orchestration.approvalRequired')}
</span>
)}
</div>
<p className="mt-1 text-xs text-stone-500 dark:text-neutral-400">
{def.description}
</p>
</div>
{!isStarting && (
<button
type="button"
data-testid={`orchestration-start-${def.id}`}
onClick={() => beginStart(def)}
className="flex-none rounded-lg bg-primary-500 px-3 py-1.5 text-xs font-semibold text-white shadow-soft hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
{t('orchestration.start')}
</button>
)}
</div>
{/* Inline start panel */}
{isStarting && (
<div className="mt-3 space-y-3 border-t border-stone-100 pt-3 dark:border-neutral-800">
<label className="block text-xs font-medium text-stone-600 dark:text-neutral-300">
{t('orchestration.questionLabel')}
<textarea
data-testid="orchestration-question"
value={question}
onChange={e => setQuestion(e.target.value)}
rows={2}
placeholder={t('orchestration.questionPlaceholder')}
className="mt-1 w-full rounded-lg border border-stone-200 bg-white px-2 py-1.5 text-sm text-stone-800 focus:border-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100"
/>
</label>
{startError && (
<p className="text-xs text-coral-600 dark:text-coral-300"> {startError}</p>
)}
{assessment.requiresApproval ? (
<WorkflowRunApprovalCard
definition={def}
reasons={assessment.reasons}
starting={starting}
onApprove={() => void doStart()}
onCancel={cancelStart}
/>
) : (
<div className="flex items-center gap-2">
<button
type="button"
data-testid="orchestration-confirm-start"
disabled={starting}
onClick={() => void doStart()}
className="rounded-lg bg-primary-500 px-3 py-1.5 text-xs font-semibold text-white shadow-soft hover:bg-primary-600 disabled:opacity-50">
{starting
? t('orchestration.starting')
: t('orchestration.confirmStart')}
</button>
<button
type="button"
onClick={cancelStart}
disabled={starting}
className="rounded-lg border border-stone-300 bg-white px-3 py-1.5 text-xs font-medium text-stone-700 hover:bg-stone-50 disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300">
{t('orchestration.approval.cancel')}
</button>
</div>
)}
</div>
)}
</li>
);
})}
</ul>
)}
</section>
{/* Selected run drill-in */}
{selectedRun && (
<section className="space-y-2" data-testid="orchestration-selected-run">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold uppercase tracking-wide text-stone-500 dark:text-neutral-400">
{t('orchestration.runProgress')}
</h3>
<button
type="button"
data-testid="orchestration-close-run"
onClick={() => {
setSelectedRunId(null);
setSelectedRun(null);
}}
className="text-[11px] text-stone-400 hover:text-stone-600 dark:text-neutral-500 dark:hover:text-neutral-300">
{t('orchestration.close')}
</button>
</div>
<WorkflowRunDetail
definition={definitions.find(d => d.id === selectedRun.definitionId)}
run={selectedRun}
busy={controlBusy}
onStop={id => void handleStop(id)}
onResume={id => void handleResume(id)}
/>
</section>
)}
{/* Recent runs */}
<section className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wide text-stone-500 dark:text-neutral-400">
{t('orchestration.recentRuns')}
</h3>
{runs.length === 0 ? (
<div className="rounded-xl border border-dashed border-stone-200 py-8 text-center text-sm text-stone-400 dark:border-neutral-800 dark:text-neutral-500">
{t('orchestration.noRuns')}
</div>
) : (
<ul
className="divide-y divide-stone-100 overflow-hidden rounded-xl border border-stone-200 bg-white dark:divide-neutral-800 dark:border-neutral-800 dark:bg-neutral-900"
data-testid="orchestration-runs">
{runs.map(run => {
const def = definitions.find(d => d.id === run.definitionId);
return (
<li key={run.id}>
<button
type="button"
data-testid={`orchestration-run-${run.id}`}
onClick={() => openRun(run)}
className={`flex w-full items-center justify-between gap-2 p-3 text-left hover:bg-stone-50 dark:hover:bg-neutral-800/60 ${
run.id === selectedRunId ? 'bg-stone-50 dark:bg-neutral-800/60' : ''
}`}>
<span className="min-w-0">
<span className="block truncate text-sm font-medium text-stone-800 dark:text-neutral-100">
{def?.name ?? run.definitionId}
</span>
<span className="font-mono text-[10px] text-stone-400 dark:text-neutral-500">
{run.id}
</span>
</span>
<span className="flex-none text-[11px] text-stone-400 dark:text-neutral-500">
{t(`orchestration.runStatus.${run.status}`)}
</span>
</button>
</li>
);
})}
</ul>
)}
</section>
</div>
);
}
@@ -0,0 +1,95 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { type WorkflowDefinition } from '../../services/api/workflowRunsApi';
import WorkflowRunApprovalCard from './WorkflowRunApprovalCard';
// i18n → echo the key so assertions can target stable strings.
vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
function def(overrides: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
return {
id: 'parallel_research_cross_check',
name: 'Parallel research',
description: 'desc',
phases: [],
defaultConcurrency: 2,
maxChildren: 8,
safetyTier: 'read_only',
...overrides,
};
}
describe('WorkflowRunApprovalCard', () => {
it('renders one localized line per reason', () => {
render(
<WorkflowRunApprovalCard
definition={def()}
reasons={['high_children', 'high_concurrency']}
onApprove={vi.fn()}
onCancel={vi.fn()}
/>
);
const reasons = screen.getByTestId('workflow-approval-reasons');
expect(reasons.querySelectorAll('li')).toHaveLength(2);
expect(screen.getByText('orchestration.approval.reason.children')).toBeInTheDocument();
expect(screen.getByText('orchestration.approval.reason.concurrency')).toBeInTheDocument();
});
it('shows the concrete cost facts (tier / concurrency / max children)', () => {
render(
<WorkflowRunApprovalCard
definition={def({ defaultConcurrency: 3, maxChildren: 8 })}
reasons={['high_children']}
onApprove={vi.fn()}
onCancel={vi.fn()}
/>
);
expect(screen.getByText('orchestration.tier.readOnly')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('8')).toBeInTheDocument();
});
it('fires onApprove when "Approve & start" is clicked', () => {
const onApprove = vi.fn();
render(
<WorkflowRunApprovalCard
definition={def()}
reasons={['high_children']}
onApprove={onApprove}
onCancel={vi.fn()}
/>
);
fireEvent.click(screen.getByTestId('workflow-approval-approve'));
expect(onApprove).toHaveBeenCalledTimes(1);
});
it('fires onCancel when "Cancel" is clicked', () => {
const onCancel = vi.fn();
render(
<WorkflowRunApprovalCard
definition={def()}
reasons={['high_children']}
onApprove={vi.fn()}
onCancel={onCancel}
/>
);
fireEvent.click(screen.getByTestId('workflow-approval-cancel'));
expect(onCancel).toHaveBeenCalledTimes(1);
});
it('disables both buttons and shows the starting label while a start is in flight', () => {
render(
<WorkflowRunApprovalCard
definition={def()}
reasons={['high_children']}
starting
onApprove={vi.fn()}
onCancel={vi.fn()}
/>
);
expect(screen.getByTestId('workflow-approval-approve')).toBeDisabled();
expect(screen.getByTestId('workflow-approval-cancel')).toBeDisabled();
expect(screen.getByText('orchestration.approval.starting')).toBeInTheDocument();
});
});
@@ -0,0 +1,148 @@
/**
* WorkflowRunApprovalCard (#3375)
* --------------------------------
*
* Confirmation surface shown before starting a high-cost / high-concurrency
* workflow run. The acceptance criterion "High-cost/high-concurrency runs
* require explicit approval" is enforced on the client: the Orchestration tab
* calls {@link assessWorkflowCost} and, when it returns `requiresApproval`,
* renders this card instead of starting immediately. The user must press
* "Approve & start" to proceed; "Cancel" aborts without touching the engine.
*
* Styling mirrors the chat ApprovalRequestCard (amber warning chrome) so the
* approval affordance is visually consistent across the app.
*/
import debug from 'debug';
import React from 'react';
import { useT } from '../../lib/i18n/I18nContext';
import {
type WorkflowCostReason,
type WorkflowDefinition,
type WorkflowSafetyTier,
} from '../../services/api/workflowRunsApi';
import Button from '../ui/Button';
const log = debug('intelligence:workflow-approval');
/** i18n key for each cost reason code. */
const REASON_KEY: Record<WorkflowCostReason, string> = {
non_read_only_tier: 'orchestration.approval.reason.tier',
high_concurrency: 'orchestration.approval.reason.concurrency',
high_children: 'orchestration.approval.reason.children',
};
/** i18n key for each safety tier label. */
export const SAFETY_TIER_KEY: Record<WorkflowSafetyTier, string> = {
read_only: 'orchestration.tier.readOnly',
standard: 'orchestration.tier.standard',
edit_capable: 'orchestration.tier.editCapable',
};
interface Props {
definition: WorkflowDefinition;
reasons: WorkflowCostReason[];
/** Whether the start RPC is in flight (disables the buttons). */
starting?: boolean;
onApprove: () => void;
onCancel: () => void;
}
export const WorkflowRunApprovalCard: React.FC<Props> = ({
definition,
reasons,
starting = false,
onApprove,
onCancel,
}) => {
const { t } = useT();
return (
<div
role="alertdialog"
aria-label={t('orchestration.approval.title')}
data-testid="workflow-approval-card"
className="rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm dark:border-amber-500/40 dark:bg-amber-500/10">
<div className="flex items-start gap-2">
<span aria-hidden className="text-base leading-none">
</span>
<div className="min-w-0 flex-1">
<p className="font-semibold text-amber-900 dark:text-amber-200">
{t('orchestration.approval.title')}
</p>
<p className="mt-1 break-words text-amber-800/90 dark:text-amber-200/90">
{t('orchestration.approval.body')}{' '}
<span className="font-semibold">{definition.name}</span>
</p>
{/* Why approval is required — one localized line per reason code. */}
<ul
data-testid="workflow-approval-reasons"
className="mt-2 list-disc space-y-1 pl-5 text-xs text-amber-800/90 dark:text-amber-200/90">
{reasons.map(reason => (
<li key={reason}>{t(REASON_KEY[reason])}</li>
))}
</ul>
{/* Concrete cost facts so the user knows what they're approving. */}
<dl className="mt-3 grid grid-cols-3 gap-2 text-xs">
<div className="rounded-md bg-amber-100/70 px-2 py-1.5 dark:bg-amber-500/15">
<dt className="text-amber-700/80 dark:text-amber-300/80">
{t('orchestration.approval.tier')}
</dt>
<dd className="font-medium text-amber-900 dark:text-amber-100">
{t(SAFETY_TIER_KEY[definition.safetyTier])}
</dd>
</div>
<div className="rounded-md bg-amber-100/70 px-2 py-1.5 dark:bg-amber-500/15">
<dt className="text-amber-700/80 dark:text-amber-300/80">
{t('orchestration.approval.concurrency')}
</dt>
<dd className="font-medium text-amber-900 dark:text-amber-100">
{definition.defaultConcurrency}
</dd>
</div>
<div className="rounded-md bg-amber-100/70 px-2 py-1.5 dark:bg-amber-500/15">
<dt className="text-amber-700/80 dark:text-amber-300/80">
{t('orchestration.approval.maxChildren')}
</dt>
<dd className="font-medium text-amber-900 dark:text-amber-100">
{definition.maxChildren}
</dd>
</div>
</dl>
<div className="mt-3 flex flex-wrap items-center gap-2">
<Button
variant="primary"
size="sm"
data-testid="workflow-approval-approve"
disabled={starting}
onClick={() => {
log('approve definitionId=%s', definition.id);
onApprove();
}}>
{starting
? t('orchestration.approval.starting')
: t('orchestration.approval.approve')}
</Button>
<Button
variant="secondary"
size="sm"
data-testid="workflow-approval-cancel"
disabled={starting}
onClick={() => {
log('cancel definitionId=%s', definition.id);
onCancel();
}}>
{t('orchestration.approval.cancel')}
</Button>
</div>
</div>
</div>
</div>
);
};
export default WorkflowRunApprovalCard;
@@ -0,0 +1,200 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { type WorkflowDefinition, type WorkflowRun } from '../../services/api/workflowRunsApi';
import WorkflowRunDetail from './WorkflowRunDetail';
// i18n → echo the key so assertions can target stable strings.
vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
function def(): WorkflowDefinition {
return {
id: 'parallel_research_cross_check',
name: 'Parallel research',
description: 'desc',
phases: [
{
name: 'decompose',
description: 'Break the question down.',
agentIds: ['planner'],
dependsOn: [],
},
{
name: 'research',
description: 'Research in parallel.',
agentIds: ['researcher', 'researcher'],
dependsOn: ['decompose'],
},
{
name: 'synthesize',
description: 'Synthesize.',
agentIds: ['summarizer'],
dependsOn: ['research'],
},
],
defaultConcurrency: 2,
maxChildren: 8,
safetyTier: 'read_only',
};
}
function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
return {
id: 'wfrun-1',
definitionId: 'parallel_research_cross_check',
parentThreadId: null,
input: { question: 'q' },
phaseStates: {
decompose: {
status: 'completed',
outputs: [{ orchestrationId: 'orch-1', agentId: 'planner', output: 'three angles' }],
},
research: { status: 'running', outputs: [] },
synthesize: { status: 'pending', outputs: [] },
},
childRunIds: ['orch-1'],
status: 'running',
summary: null,
startedAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:01:00Z',
completedAt: null,
...overrides,
};
}
describe('WorkflowRunDetail — progress rendering', () => {
it('renders the run status and every declared phase in order', () => {
render(
<WorkflowRunDetail definition={def()} run={run()} onStop={vi.fn()} onResume={vi.fn()} />
);
expect(screen.getByTestId('workflow-run-status')).toHaveTextContent(
'orchestration.runStatus.running'
);
const phases = screen.getByTestId('workflow-phase-list').querySelectorAll('li');
expect(phases).toHaveLength(3);
expect(screen.getByTestId('workflow-phase-decompose')).toBeInTheDocument();
expect(screen.getByTestId('workflow-phase-research')).toBeInTheDocument();
expect(screen.getByTestId('workflow-phase-synthesize')).toBeInTheDocument();
});
it('reflects per-phase status from phaseStates', () => {
render(
<WorkflowRunDetail definition={def()} run={run()} onStop={vi.fn()} onResume={vi.fn()} />
);
expect(screen.getByTestId('workflow-phase-status-decompose')).toHaveTextContent(
'orchestration.phaseStatus.completed'
);
expect(screen.getByTestId('workflow-phase-status-research')).toHaveTextContent(
'orchestration.phaseStatus.running'
);
expect(screen.getByTestId('workflow-phase-status-synthesize')).toHaveTextContent(
'orchestration.phaseStatus.pending'
);
});
it('shows child agent refs for a phase when expanded', () => {
render(
<WorkflowRunDetail definition={def()} run={run()} onStop={vi.fn()} onResume={vi.fn()} />
);
// Outputs are collapsed by default; expand the completed phase.
fireEvent.click(screen.getByTestId('workflow-phase-decompose').querySelector('button')!);
const outputs = screen.getByTestId('workflow-phase-outputs-decompose');
expect(outputs).toHaveTextContent('planner');
expect(outputs).toHaveTextContent('orch-1');
expect(outputs).toHaveTextContent('three angles');
});
it('renders the run-level child agent ref count', () => {
render(
<WorkflowRunDetail definition={def()} run={run()} onStop={vi.fn()} onResume={vi.fn()} />
);
expect(screen.getByTestId('workflow-child-refs')).toBeInTheDocument();
});
it('shows the final synthesis only once the run is terminal with a summary', () => {
const { rerender } = render(
<WorkflowRunDetail definition={def()} run={run()} onStop={vi.fn()} onResume={vi.fn()} />
);
expect(screen.queryByTestId('workflow-run-summary')).not.toBeInTheDocument();
rerender(
<WorkflowRunDetail
definition={def()}
run={run({
status: 'completed',
summary: 'A cited report.',
completedAt: '2026-01-01T00:05:00Z',
})}
onStop={vi.fn()}
onResume={vi.fn()}
/>
);
expect(screen.getByTestId('workflow-run-summary')).toHaveTextContent('A cited report.');
});
it('offers Stop while running and wires it to onStop', () => {
const onStop = vi.fn();
render(<WorkflowRunDetail definition={def()} run={run()} onStop={onStop} onResume={vi.fn()} />);
expect(screen.queryByTestId('workflow-run-resume')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('workflow-run-stop'));
expect(onStop).toHaveBeenCalledWith('wfrun-1');
});
it('offers Resume when interrupted and wires it to onResume', () => {
const onResume = vi.fn();
render(
<WorkflowRunDetail
definition={def()}
run={run({ status: 'interrupted' })}
onStop={vi.fn()}
onResume={onResume}
/>
);
expect(screen.queryByTestId('workflow-run-stop')).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId('workflow-run-resume'));
expect(onResume).toHaveBeenCalledWith('wfrun-1');
});
it('falls back to phaseStates keys + raw phase name when the definition is missing', () => {
render(
<WorkflowRunDetail
definition={undefined}
run={run({
phaseStates: { decompose: { status: 'running', outputs: [] } },
childRunIds: [],
})}
onStop={vi.fn()}
onResume={vi.fn()}
/>
);
// With no definition, the phase order comes from run.phaseStates and the
// label falls back to the raw phase name (WorkflowRunDetail.tsx:173).
const phases = screen.getByTestId('workflow-phase-list').querySelectorAll('li');
expect(phases).toHaveLength(1);
expect(screen.getByTestId('workflow-phase-decompose')).toHaveTextContent('decompose');
});
it('renders the failure reason for a failed phase', () => {
render(
<WorkflowRunDetail
definition={def()}
run={run({
status: 'failed',
phaseStates: {
decompose: { status: 'failed', outputs: [], reason: 'planner agent errored out' },
research: { status: 'pending', outputs: [] },
synthesize: { status: 'pending', outputs: [] },
},
})}
onStop={vi.fn()}
onResume={vi.fn()}
/>
);
// The failed-phase reason banner (WorkflowRunDetail.tsx:197).
expect(screen.getByTestId('workflow-phase-decompose')).toHaveTextContent(
'planner agent errored out'
);
});
});
@@ -0,0 +1,262 @@
/**
* WorkflowRunDetail (#3375)
* -------------------------
*
* Drill-in view for a single workflow run. Given a definition (for phase
* ordering + labels) and the latest run snapshot, it renders:
* - the run status header,
* - an ordered phase timeline driven by `run.phaseStates`,
* - each phase's child agent refs (orchestration id + agent id + output),
* - Stop / Resume controls wired to the engine,
* - the final synthesized report once the run completes.
*
* The component is presentational + control-only: the parent (Orchestration
* tab) owns the polling loop and passes a fresh `run` each tick. Stop / Resume
* delegate to callbacks so the parent can refresh its run list too.
*/
import debug from 'debug';
import React, { useState } from 'react';
import { useT } from '../../lib/i18n/I18nContext';
import {
type WorkflowDefinition,
type WorkflowPhaseState,
type WorkflowPhaseStatus,
type WorkflowRun,
type WorkflowRunStatus,
} from '../../services/api/workflowRunsApi';
const log = debug('intelligence:workflow-detail');
/** Accent classes per run status (semantic palette from tailwind.config.js). */
const RUN_STATUS_ACCENT: Record<WorkflowRunStatus, string> = {
pending:
'border-stone-200 bg-stone-50 text-stone-600 dark:border-neutral-700 dark:bg-neutral-800/60 dark:text-neutral-300',
running:
'border-ocean-200 bg-ocean-50 text-ocean-700 dark:border-ocean-500/30 dark:bg-ocean-500/10 dark:text-ocean-300',
completed:
'border-sage-200 bg-sage-50 text-sage-700 dark:border-sage-500/30 dark:bg-sage-500/10 dark:text-sage-300',
failed:
'border-coral-200 bg-coral-50 text-coral-700 dark:border-coral-500/30 dark:bg-coral-500/10 dark:text-coral-300',
cancelled:
'border-stone-200 bg-stone-50 text-stone-600 dark:border-neutral-700 dark:bg-neutral-800/60 dark:text-neutral-300',
interrupted:
'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300',
};
const RUN_STATUS_KEY: Record<WorkflowRunStatus, string> = {
pending: 'orchestration.runStatus.pending',
running: 'orchestration.runStatus.running',
completed: 'orchestration.runStatus.completed',
failed: 'orchestration.runStatus.failed',
cancelled: 'orchestration.runStatus.cancelled',
interrupted: 'orchestration.runStatus.interrupted',
};
const PHASE_STATUS_KEY: Record<WorkflowPhaseStatus, string> = {
pending: 'orchestration.phaseStatus.pending',
running: 'orchestration.phaseStatus.running',
completed: 'orchestration.phaseStatus.completed',
failed: 'orchestration.phaseStatus.failed',
};
/** Glyph per phase status — color comes from the surrounding classes. */
const PHASE_STATUS_DOT: Record<WorkflowPhaseStatus, string> = {
pending: 'bg-stone-300 dark:bg-neutral-600',
running: 'bg-ocean-500 animate-pulse',
completed: 'bg-sage-500',
failed: 'bg-coral-500',
};
const TERMINAL_STATUSES: WorkflowRunStatus[] = ['completed', 'failed', 'cancelled', 'interrupted'];
interface Props {
definition: WorkflowDefinition | undefined;
run: WorkflowRun;
/** True while a stop/resume RPC is in flight. */
busy?: boolean;
onStop: (id: string) => void;
onResume: (id: string) => void;
}
export const WorkflowRunDetail: React.FC<Props> = ({
definition,
run,
busy = false,
onStop,
onResume,
}) => {
const { t } = useT();
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
// Phase order: lead with the definition's declared order, then append any
// runtime-only phases the run reports but the definition doesn't list (so a
// run still renders its full progress during definition/version drift).
const declaredPhaseNames = definition?.phases.map(p => p.name) ?? [];
const declaredSet = new Set(declaredPhaseNames);
const runtimeOnlyPhaseNames = Object.keys(run.phaseStates).filter(name => !declaredSet.has(name));
const phaseNames = [...declaredPhaseNames, ...runtimeOnlyPhaseNames];
const isRunning = run.status === 'running' || run.status === 'pending';
const canResume = run.status === 'interrupted';
const toggle = (name: string) => setExpanded(prev => ({ ...prev, [name]: !prev[name] }));
return (
<div className="space-y-4" data-testid="workflow-run-detail">
{/* Header: status + controls */}
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
data-testid="workflow-run-status"
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${RUN_STATUS_ACCENT[run.status]}`}>
{run.status === 'running' && (
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-ocean-500" />
)}
{t(RUN_STATUS_KEY[run.status])}
</span>
<span className="font-mono text-[11px] text-stone-400 dark:text-neutral-500">
{run.id}
</span>
</div>
<div className="flex items-center gap-2">
{isRunning && (
<button
type="button"
data-testid="workflow-run-stop"
disabled={busy}
onClick={() => {
log('stop id=%s', run.id);
onStop(run.id);
}}
className="rounded-lg border border-coral-300 bg-white px-3 py-1.5 text-xs font-medium text-coral-700 hover:bg-coral-50 disabled:opacity-50 dark:border-coral-700 dark:bg-neutral-900 dark:text-coral-300 dark:hover:bg-coral-900/40">
{t('orchestration.detail.stop')}
</button>
)}
{canResume && (
<button
type="button"
data-testid="workflow-run-resume"
disabled={busy}
onClick={() => {
log('resume id=%s', run.id);
onResume(run.id);
}}
className="rounded-lg border border-ocean-300 bg-white px-3 py-1.5 text-xs font-medium text-ocean-700 hover:bg-ocean-50 disabled:opacity-50 dark:border-ocean-700 dark:bg-neutral-900 dark:text-ocean-300 dark:hover:bg-ocean-900/40">
{t('orchestration.detail.resume')}
</button>
)}
</div>
</div>
{/* Phase timeline */}
<ol className="space-y-2" data-testid="workflow-phase-list">
{phaseNames.map(name => {
const phaseDef = definition?.phases.find(p => p.name === name);
const state: WorkflowPhaseState = run.phaseStates[name] ?? {
status: 'pending',
outputs: [],
};
const isOpen = expanded[name] ?? false;
const hasOutputs = state.outputs.length > 0;
return (
<li
key={name}
data-testid={`workflow-phase-${name}`}
className="rounded-xl border border-stone-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
<button
type="button"
onClick={() => toggle(name)}
className="flex w-full items-center justify-between gap-2 px-3 py-2 text-left">
<span className="flex min-w-0 items-center gap-2">
<span
className={`h-2 w-2 flex-none rounded-full ${PHASE_STATUS_DOT[state.status]}`}
/>
<span className="truncate text-sm font-medium text-stone-800 dark:text-neutral-100">
{phaseDef?.name ?? name}
</span>
<span
data-testid={`workflow-phase-status-${name}`}
className="rounded-md border border-stone-200 px-1.5 py-0.5 text-[10px] font-medium text-stone-500 dark:border-neutral-700 dark:text-neutral-400">
{t(PHASE_STATUS_KEY[state.status])}
</span>
</span>
<span className="flex flex-none items-center gap-2 text-[11px] text-stone-400 dark:text-neutral-500">
{hasOutputs && (
<span data-testid={`workflow-phase-count-${name}`}>
{state.outputs.length} {t('orchestration.detail.agents')}
</span>
)}
<span aria-hidden>{isOpen ? '▾' : '▸'}</span>
</span>
</button>
{phaseDef?.description && (
<p className="px-3 pb-1 text-xs text-stone-500 dark:text-neutral-400">
{phaseDef.description}
</p>
)}
{state.status === 'failed' && state.reason && (
<p className="mx-3 mb-2 rounded-md bg-coral-50 px-2 py-1 text-xs text-coral-700 dark:bg-coral-500/10 dark:text-coral-300">
{state.reason}
</p>
)}
{/* Child agent refs for this phase */}
{isOpen && hasOutputs && (
<ul className="space-y-2 px-3 pb-3" data-testid={`workflow-phase-outputs-${name}`}>
{state.outputs.map((out, idx) => (
<li
key={`${out.orchestrationId}-${idx}`}
className="rounded-lg border border-stone-100 bg-stone-50 p-2 dark:border-neutral-800 dark:bg-neutral-800/40">
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs font-medium text-stone-700 dark:text-neutral-200">
{out.agentId}
</span>
<span className="font-mono text-[10px] text-stone-400 dark:text-neutral-500">
{out.orchestrationId}
</span>
</div>
{out.output && (
<p className="mt-1 whitespace-pre-wrap break-words text-xs leading-snug text-stone-600 dark:text-neutral-300">
{out.output}
</p>
)}
</li>
))}
</ul>
)}
</li>
);
})}
</ol>
{/* Child agent refs summary (full run-level list) */}
{run.childRunIds.length > 0 && (
<div
className="text-[11px] text-stone-400 dark:text-neutral-500"
data-testid="workflow-child-refs">
{t('orchestration.detail.childRefs')}: {run.childRunIds.length}
</div>
)}
{/* Final synthesis */}
{run.summary && TERMINAL_STATUSES.includes(run.status) && (
<div
data-testid="workflow-run-summary"
className="rounded-xl border border-sage-200 bg-sage-50 p-3 dark:border-sage-500/30 dark:bg-sage-500/10">
<p className="mb-1 text-xs font-semibold text-sage-800 dark:text-sage-200">
{t('orchestration.detail.synthesis')}
</p>
<p className="whitespace-pre-wrap break-words text-sm leading-snug text-stone-700 dark:text-neutral-200">
{run.summary}
</p>
</div>
)}
</div>
);
};
export default WorkflowRunDetail;
+49
View File
@@ -5360,6 +5360,55 @@ const messages: TranslationMap = {
'notch.speaking': 'أتحدث…',
'notch.transcribing': 'أفسّر…',
'notch.executing': 'أنفّذ…',
'memory.tab.orchestration': 'التنسيق',
'memory.tab.orchestrationDescription':
'شغّل سير عمل متعدد الوكلاء — وزّع سؤالاً على وكلاء متوازين، وتحقّق من نتائجهم بشكل متبادل، وراقب اكتمال كل مرحلة لتتجمّع في إجابة واحدة مُركَّبة.',
'orchestration.subtitle':
'ابدأ سير عمل متعدد الوكلاء، وراقب تقدّم مراحله، واقرأ النتيجة المُركَّبة.',
'orchestration.loading': 'جارٍ تحميل سير العمل…',
'orchestration.failedToLoad': 'تعذّر تحميل سير العمل',
'orchestration.definitions': 'سير العمل المتاح',
'orchestration.noDefinitions': 'لا توجد تعريفات سير عمل متاحة.',
'orchestration.approvalRequired': 'الموافقة مطلوبة',
'orchestration.start': 'بدء',
'orchestration.confirmStart': 'بدء التشغيل',
'orchestration.starting': 'جارٍ البدء…',
'orchestration.questionLabel': 'سؤال البحث (اختياري)',
'orchestration.questionPlaceholder': 'ما الذي يجب أن يبحث فيه الوكلاء؟',
'orchestration.runProgress': 'تقدّم التشغيل',
'orchestration.recentRuns': 'عمليات التشغيل الأخيرة',
'orchestration.noRuns': 'لا توجد عمليات تشغيل لسير العمل بعد.',
'orchestration.close': 'إغلاق',
'orchestration.tier.readOnly': 'للقراءة فقط',
'orchestration.tier.standard': 'قياسي',
'orchestration.tier.editCapable': 'قادر على التحرير',
'orchestration.approval.title': 'الموافقة على تشغيل سير العمل هذا',
'orchestration.approval.body':
'هذا تشغيل عالي التكلفة أو عالي التزامن ويتطلّب موافقتك الصريحة قبل أن يبدأ:',
'orchestration.approval.reason.tier': 'يمكن لوكلائه تنفيذ إجراءات تتجاوز البحث للقراءة فقط.',
'orchestration.approval.reason.concurrency': 'يشغّل العديد من الوكلاء في الوقت نفسه.',
'orchestration.approval.reason.children': 'قد يُنشئ عدداً كبيراً جداً من الوكلاء إجمالاً.',
'orchestration.approval.tier': 'مستوى الأمان',
'orchestration.approval.concurrency': 'التزامن',
'orchestration.approval.maxChildren': 'الحد الأقصى للوكلاء',
'orchestration.approval.approve': 'الموافقة والبدء',
'orchestration.approval.starting': 'جارٍ البدء…',
'orchestration.approval.cancel': 'إلغاء',
'orchestration.runStatus.pending': 'قيد الانتظار',
'orchestration.runStatus.running': 'قيد التشغيل',
'orchestration.runStatus.completed': 'مكتمل',
'orchestration.runStatus.failed': 'فشل',
'orchestration.runStatus.cancelled': 'مُلغى',
'orchestration.runStatus.interrupted': 'مُقاطع',
'orchestration.phaseStatus.pending': 'قيد الانتظار',
'orchestration.phaseStatus.running': 'قيد التشغيل',
'orchestration.phaseStatus.completed': 'مكتملة',
'orchestration.phaseStatus.failed': 'فشلت',
'orchestration.detail.stop': 'إيقاف',
'orchestration.detail.resume': 'استئناف',
'orchestration.detail.agents': 'وكلاء',
'orchestration.detail.childRefs': 'الوكلاء الفرعيون',
'orchestration.detail.synthesis': 'التركيب النهائي',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'ملفات الوكيل',
'settings.profiles.subtitle': 'وكلاء بطابع خاص — لكل منهم روحه وذاكرته وموصّلاته ومهاراته.',
+49
View File
@@ -5470,6 +5470,55 @@ const messages: TranslationMap = {
'notch.speaking': 'বলছি…',
'notch.transcribing': 'ট্রান্সক্রাইব করছি…',
'notch.executing': 'চালাচ্ছি…',
'memory.tab.orchestration': 'অর্কেস্ট্রেশন',
'memory.tab.orchestrationDescription':
'বহু-এজেন্ট ওয়ার্কফ্লো চালান — একটি প্রশ্নকে সমান্তরাল এজেন্টদের মধ্যে ছড়িয়ে দিন, তাদের ফলাফল পরস্পর যাচাই করুন, এবং দেখুন প্রতিটি ধাপ কীভাবে একটি সমন্বিত উত্তরে সম্পূর্ণ হয়।',
'orchestration.subtitle':
'একটি বহু-এজেন্ট ওয়ার্কফ্লো শুরু করুন, এর ধাপগুলোর অগ্রগতি দেখুন, এবং সমন্বিত ফলাফল পড়ুন।',
'orchestration.loading': 'ওয়ার্কফ্লো লোড হচ্ছে…',
'orchestration.failedToLoad': 'ওয়ার্কফ্লো লোড করতে ব্যর্থ',
'orchestration.definitions': 'উপলব্ধ ওয়ার্কফ্লো',
'orchestration.noDefinitions': 'কোনো ওয়ার্কফ্লো সংজ্ঞা উপলব্ধ নেই।',
'orchestration.approvalRequired': 'অনুমোদন প্রয়োজন',
'orchestration.start': 'শুরু করুন',
'orchestration.confirmStart': 'রান শুরু করুন',
'orchestration.starting': 'শুরু হচ্ছে…',
'orchestration.questionLabel': 'গবেষণা প্রশ্ন (ঐচ্ছিক)',
'orchestration.questionPlaceholder': 'এজেন্টদের কী গবেষণা করা উচিত?',
'orchestration.runProgress': 'রানের অগ্রগতি',
'orchestration.recentRuns': 'সাম্প্রতিক রান',
'orchestration.noRuns': 'এখনও কোনো ওয়ার্কফ্লো রান নেই।',
'orchestration.close': 'বন্ধ করুন',
'orchestration.tier.readOnly': 'শুধু-পড়া',
'orchestration.tier.standard': 'মানক',
'orchestration.tier.editCapable': 'সম্পাদনা-সক্ষম',
'orchestration.approval.title': 'এই ওয়ার্কফ্লো রান অনুমোদন করুন',
'orchestration.approval.body':
'এটি একটি উচ্চ-ব্যয় বা উচ্চ-সমান্তরালতার রান এবং শুরুর আগে এর জন্য আপনার স্পষ্ট অনুমোদন প্রয়োজন:',
'orchestration.approval.reason.tier': 'এর এজেন্টরা শুধু-পড়া গবেষণার বাইরেও পদক্ষেপ নিতে পারে।',
'orchestration.approval.reason.concurrency': 'এটি একই সময়ে অনেক এজেন্ট চালায়।',
'orchestration.approval.reason.children': 'এটি সর্বমোট বিপুল সংখ্যক এজেন্ট তৈরি করতে পারে।',
'orchestration.approval.tier': 'নিরাপত্তা স্তর',
'orchestration.approval.concurrency': 'সমান্তরালতা',
'orchestration.approval.maxChildren': 'সর্বোচ্চ এজেন্ট',
'orchestration.approval.approve': 'অনুমোদন ও শুরু করুন',
'orchestration.approval.starting': 'শুরু হচ্ছে…',
'orchestration.approval.cancel': 'বাতিল',
'orchestration.runStatus.pending': 'অপেক্ষমাণ',
'orchestration.runStatus.running': 'চলছে',
'orchestration.runStatus.completed': 'সম্পন্ন',
'orchestration.runStatus.failed': 'ব্যর্থ',
'orchestration.runStatus.cancelled': 'বাতিল',
'orchestration.runStatus.interrupted': 'বিঘ্নিত',
'orchestration.phaseStatus.pending': 'অপেক্ষমাণ',
'orchestration.phaseStatus.running': 'চলছে',
'orchestration.phaseStatus.completed': 'সম্পন্ন',
'orchestration.phaseStatus.failed': 'ব্যর্থ',
'orchestration.detail.stop': 'থামান',
'orchestration.detail.resume': 'পুনরায় শুরু করুন',
'orchestration.detail.agents': 'এজেন্ট',
'orchestration.detail.childRefs': 'চাইল্ড এজেন্ট',
'orchestration.detail.synthesis': 'চূড়ান্ত সংশ্লেষণ',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'এজেন্ট প্রোফাইল',
'settings.profiles.subtitle':
+50
View File
@@ -5619,6 +5619,56 @@ const messages: TranslationMap = {
'notch.speaking': 'Spreche…',
'notch.transcribing': 'Transkribiere…',
'notch.executing': 'Führe aus…',
'memory.tab.orchestration': 'Orchestrierung',
'memory.tab.orchestrationDescription':
'Führe Multi-Agenten-Workflows aus — verteile eine Frage auf parallele Agenten, gleiche ihre Ergebnisse ab und sieh zu, wie jede Phase zu einer zusammengeführten Antwort wird.',
'orchestration.subtitle':
'Starte einen Multi-Agenten-Workflow, verfolge seinen Phasenfortschritt und lies das zusammengeführte Ergebnis.',
'orchestration.loading': 'Lade Workflows…',
'orchestration.failedToLoad': 'Workflows konnten nicht geladen werden',
'orchestration.definitions': 'Verfügbare Workflows',
'orchestration.noDefinitions': 'Keine Workflow-Definitionen verfügbar.',
'orchestration.approvalRequired': 'Genehmigung erforderlich',
'orchestration.start': 'Starten',
'orchestration.confirmStart': 'Lauf starten',
'orchestration.starting': 'Wird gestartet…',
'orchestration.questionLabel': 'Forschungsfrage (optional)',
'orchestration.questionPlaceholder': 'Was sollen die Agenten erforschen?',
'orchestration.runProgress': 'Lauffortschritt',
'orchestration.recentRuns': 'Letzte Läufe',
'orchestration.noRuns': 'Noch keine Workflow-Läufe.',
'orchestration.close': 'Schließen',
'orchestration.tier.readOnly': 'Nur lesen',
'orchestration.tier.standard': 'Standard',
'orchestration.tier.editCapable': 'Bearbeitungsfähig',
'orchestration.approval.title': 'Diesen Workflow-Lauf genehmigen',
'orchestration.approval.body':
'Dies ist ein kosten- oder parallelitätsintensiver Lauf und benötigt vor dem Start deine ausdrückliche Genehmigung:',
'orchestration.approval.reason.tier':
'Seine Agenten können über reine Lese-Recherche hinaus Aktionen ausführen.',
'orchestration.approval.reason.concurrency': 'Er führt viele Agenten gleichzeitig aus.',
'orchestration.approval.reason.children': 'Er kann insgesamt sehr viele Agenten erzeugen.',
'orchestration.approval.tier': 'Sicherheitsstufe',
'orchestration.approval.concurrency': 'Parallelität',
'orchestration.approval.maxChildren': 'Max. Agenten',
'orchestration.approval.approve': 'Genehmigen & starten',
'orchestration.approval.starting': 'Wird gestartet…',
'orchestration.approval.cancel': 'Abbrechen',
'orchestration.runStatus.pending': 'Ausstehend',
'orchestration.runStatus.running': 'Läuft',
'orchestration.runStatus.completed': 'Abgeschlossen',
'orchestration.runStatus.failed': 'Fehlgeschlagen',
'orchestration.runStatus.cancelled': 'Abgebrochen',
'orchestration.runStatus.interrupted': 'Unterbrochen',
'orchestration.phaseStatus.pending': 'Ausstehend',
'orchestration.phaseStatus.running': 'Läuft',
'orchestration.phaseStatus.completed': 'Abgeschlossen',
'orchestration.phaseStatus.failed': 'Fehlgeschlagen',
'orchestration.detail.stop': 'Stoppen',
'orchestration.detail.resume': 'Fortsetzen',
'orchestration.detail.agents': 'Agenten',
'orchestration.detail.childRefs': 'Untergeordnete Agenten',
'orchestration.detail.synthesis': 'Endgültige Zusammenführung',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Agentenprofile',
'settings.profiles.subtitle':
+50
View File
@@ -464,6 +464,9 @@ const en: TranslationMap = {
'memory.tab.workflows': 'Workflows',
'memory.tab.workflowsDescription':
'Reusable, runnable procedures — a goal plus the steps to reach it. Create one, install from a URL, or open a workflow to run it.',
'memory.tab.orchestration': 'Orchestration',
'memory.tab.orchestrationDescription':
'Run multi-agent workflows — fan a question out across parallel agents, cross-check their findings, and watch each phase finish into one synthesized answer.',
'memory.tab.dreams': 'Dreams',
'memory.tab.calls': 'Calls',
'memory.tab.diagram': 'Diagram',
@@ -3557,6 +3560,53 @@ const en: TranslationMap = {
'intelligence.agentWork.action.continuePlaceholder': 'Reply to unblock this agent…',
'intelligence.agentWork.action.followUpPlaceholder': 'Send a follow-up instruction…',
'intelligence.agentWork.action.failed': 'Action failed',
// Orchestration tab (#3375) — multi-agent workflow runs.
'orchestration.subtitle':
'Start a multi-agent workflow, watch its phases progress, and read the synthesized result.',
'orchestration.loading': 'Loading workflows…',
'orchestration.failedToLoad': 'Failed to load workflows',
'orchestration.definitions': 'Available workflows',
'orchestration.noDefinitions': 'No workflow definitions are available.',
'orchestration.approvalRequired': 'Approval required',
'orchestration.start': 'Start',
'orchestration.confirmStart': 'Start run',
'orchestration.starting': 'Starting…',
'orchestration.questionLabel': 'Research question (optional)',
'orchestration.questionPlaceholder': 'What should the agents research?',
'orchestration.runProgress': 'Run progress',
'orchestration.recentRuns': 'Recent runs',
'orchestration.noRuns': 'No workflow runs yet.',
'orchestration.close': 'Close',
'orchestration.tier.readOnly': 'Read-only',
'orchestration.tier.standard': 'Standard',
'orchestration.tier.editCapable': 'Edit-capable',
'orchestration.approval.title': 'Approve this workflow run',
'orchestration.approval.body':
'This is a high-cost or high-concurrency run and needs your explicit approval before it starts:',
'orchestration.approval.reason.tier': 'Its agents can take actions beyond read-only research.',
'orchestration.approval.reason.concurrency': 'It runs many agents at the same time.',
'orchestration.approval.reason.children': 'It can spawn a large number of agents in total.',
'orchestration.approval.tier': 'Safety tier',
'orchestration.approval.concurrency': 'Concurrency',
'orchestration.approval.maxChildren': 'Max agents',
'orchestration.approval.approve': 'Approve & start',
'orchestration.approval.starting': 'Starting…',
'orchestration.approval.cancel': 'Cancel',
'orchestration.runStatus.pending': 'Pending',
'orchestration.runStatus.running': 'Running',
'orchestration.runStatus.completed': 'Completed',
'orchestration.runStatus.failed': 'Failed',
'orchestration.runStatus.cancelled': 'Cancelled',
'orchestration.runStatus.interrupted': 'Interrupted',
'orchestration.phaseStatus.pending': 'Pending',
'orchestration.phaseStatus.running': 'Running',
'orchestration.phaseStatus.completed': 'Completed',
'orchestration.phaseStatus.failed': 'Failed',
'orchestration.detail.stop': 'Stop',
'orchestration.detail.resume': 'Resume',
'orchestration.detail.agents': 'agents',
'orchestration.detail.childRefs': 'Child agents',
'orchestration.detail.synthesis': 'Final synthesis',
'intelligence.teams.subtitle': 'Coordinated agent teams and the tasks they share.',
'intelligence.teams.loading': 'Loading teams…',
'intelligence.teams.failedToLoad': 'Failed to load teams',
+50
View File
@@ -5584,6 +5584,56 @@ const messages: TranslationMap = {
'notch.speaking': 'Hablando…',
'notch.transcribing': 'Transcribiendo…',
'notch.executing': 'Ejecutando…',
'memory.tab.orchestration': 'Orquestación',
'memory.tab.orchestrationDescription':
'Ejecuta flujos de trabajo con múltiples agentes: distribuye una pregunta entre agentes paralelos, contrasta sus hallazgos y observa cómo cada fase culmina en una única respuesta sintetizada.',
'orchestration.subtitle':
'Inicia un flujo de trabajo con múltiples agentes, observa el avance de sus fases y lee el resultado sintetizado.',
'orchestration.loading': 'Cargando flujos de trabajo…',
'orchestration.failedToLoad': 'No se pudieron cargar los flujos de trabajo',
'orchestration.definitions': 'Flujos de trabajo disponibles',
'orchestration.noDefinitions': 'No hay definiciones de flujos de trabajo disponibles.',
'orchestration.approvalRequired': 'Aprobación requerida',
'orchestration.start': 'Iniciar',
'orchestration.confirmStart': 'Iniciar ejecución',
'orchestration.starting': 'Iniciando…',
'orchestration.questionLabel': 'Pregunta de investigación (opcional)',
'orchestration.questionPlaceholder': '¿Qué deben investigar los agentes?',
'orchestration.runProgress': 'Progreso de la ejecución',
'orchestration.recentRuns': 'Ejecuciones recientes',
'orchestration.noRuns': 'Aún no hay ejecuciones de flujos de trabajo.',
'orchestration.close': 'Cerrar',
'orchestration.tier.readOnly': 'Solo lectura',
'orchestration.tier.standard': 'Estándar',
'orchestration.tier.editCapable': 'Con capacidad de edición',
'orchestration.approval.title': 'Aprobar esta ejecución del flujo de trabajo',
'orchestration.approval.body':
'Esta es una ejecución de alto costo o alta concurrencia y necesita tu aprobación explícita antes de iniciar:',
'orchestration.approval.reason.tier':
'Sus agentes pueden realizar acciones más allá de la investigación de solo lectura.',
'orchestration.approval.reason.concurrency': 'Ejecuta muchos agentes al mismo tiempo.',
'orchestration.approval.reason.children': 'Puede generar una gran cantidad de agentes en total.',
'orchestration.approval.tier': 'Nivel de seguridad',
'orchestration.approval.concurrency': 'Concurrencia',
'orchestration.approval.maxChildren': 'Máx. de agentes',
'orchestration.approval.approve': 'Aprobar e iniciar',
'orchestration.approval.starting': 'Iniciando…',
'orchestration.approval.cancel': 'Cancelar',
'orchestration.runStatus.pending': 'Pendiente',
'orchestration.runStatus.running': 'En ejecución',
'orchestration.runStatus.completed': 'Completada',
'orchestration.runStatus.failed': 'Fallida',
'orchestration.runStatus.cancelled': 'Cancelada',
'orchestration.runStatus.interrupted': 'Interrumpida',
'orchestration.phaseStatus.pending': 'Pendiente',
'orchestration.phaseStatus.running': 'En ejecución',
'orchestration.phaseStatus.completed': 'Completada',
'orchestration.phaseStatus.failed': 'Fallida',
'orchestration.detail.stop': 'Detener',
'orchestration.detail.resume': 'Reanudar',
'orchestration.detail.agents': 'agentes',
'orchestration.detail.childRefs': 'Agentes secundarios',
'orchestration.detail.synthesis': 'Síntesis final',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Perfiles de agente',
'settings.profiles.subtitle':
+51
View File
@@ -5604,6 +5604,57 @@ const messages: TranslationMap = {
'notch.speaking': 'Je parle…',
'notch.transcribing': 'Transcription…',
'notch.executing': "J'exécute…",
'memory.tab.orchestration': 'Orchestration',
'memory.tab.orchestrationDescription':
'Exécutez des workflows multi-agents — répartissez une question entre des agents parallèles, recoupez leurs résultats et regardez chaque phase aboutir à une réponse synthétisée unique.',
'orchestration.subtitle':
'Démarrez un workflow multi-agents, suivez la progression de ses phases et lisez le résultat synthétisé.',
'orchestration.loading': 'Chargement des workflows…',
'orchestration.failedToLoad': 'Échec du chargement des workflows',
'orchestration.definitions': 'Workflows disponibles',
'orchestration.noDefinitions': 'Aucune définition de workflow disponible.',
'orchestration.approvalRequired': 'Approbation requise',
'orchestration.start': 'Démarrer',
'orchestration.confirmStart': "Démarrer l'exécution",
'orchestration.starting': 'Démarrage…',
'orchestration.questionLabel': 'Question de recherche (facultatif)',
'orchestration.questionPlaceholder': 'Que doivent rechercher les agents ?',
'orchestration.runProgress': "Progression de l'exécution",
'orchestration.recentRuns': 'Exécutions récentes',
'orchestration.noRuns': 'Aucune exécution de workflow pour le moment.',
'orchestration.close': 'Fermer',
'orchestration.tier.readOnly': 'Lecture seule',
'orchestration.tier.standard': 'Standard',
'orchestration.tier.editCapable': "Capable d'édition",
'orchestration.approval.title': 'Approuver cette exécution de workflow',
'orchestration.approval.body':
"Il s'agit d'une exécution coûteuse ou très concurrente qui nécessite votre approbation explicite avant de démarrer :",
'orchestration.approval.reason.tier':
'Ses agents peuvent effectuer des actions au-delà de la recherche en lecture seule.',
'orchestration.approval.reason.concurrency': 'Il exécute de nombreux agents en même temps.',
'orchestration.approval.reason.children':
"Il peut générer un très grand nombre d'agents au total.",
'orchestration.approval.tier': 'Niveau de sécurité',
'orchestration.approval.concurrency': 'Concurrence',
'orchestration.approval.maxChildren': "Nombre max d'agents",
'orchestration.approval.approve': 'Approuver et démarrer',
'orchestration.approval.starting': 'Démarrage…',
'orchestration.approval.cancel': 'Annuler',
'orchestration.runStatus.pending': 'En attente',
'orchestration.runStatus.running': 'En cours',
'orchestration.runStatus.completed': 'Terminée',
'orchestration.runStatus.failed': 'Échouée',
'orchestration.runStatus.cancelled': 'Annulée',
'orchestration.runStatus.interrupted': 'Interrompue',
'orchestration.phaseStatus.pending': 'En attente',
'orchestration.phaseStatus.running': 'En cours',
'orchestration.phaseStatus.completed': 'Terminée',
'orchestration.phaseStatus.failed': 'Échouée',
'orchestration.detail.stop': 'Arrêter',
'orchestration.detail.resume': 'Reprendre',
'orchestration.detail.agents': 'agents',
'orchestration.detail.childRefs': 'Agents enfants',
'orchestration.detail.synthesis': 'Synthèse finale',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': "Profils d'agent",
'settings.profiles.subtitle':
+50
View File
@@ -5471,6 +5471,56 @@ const messages: TranslationMap = {
'notch.speaking': 'बोल रहा हूं…',
'notch.transcribing': 'ट्रांसक्राइब कर रहा हूं…',
'notch.executing': 'चला रहा हूं…',
'memory.tab.orchestration': 'ऑर्केस्ट्रेशन',
'memory.tab.orchestrationDescription':
'बहु-एजेंट वर्कफ़्लो चलाएं — एक प्रश्न को समानांतर एजेंटों में बाँटें, उनके निष्कर्षों की परस्पर जाँच करें, और देखें कि हर चरण एक संयोजित उत्तर में पूरा होता है।',
'orchestration.subtitle':
'बहु-एजेंट वर्कफ़्लो शुरू करें, उसके चरणों की प्रगति देखें, और संयोजित परिणाम पढ़ें।',
'orchestration.loading': 'वर्कफ़्लो लोड हो रहे हैं…',
'orchestration.failedToLoad': 'वर्कफ़्लो लोड करने में विफल',
'orchestration.definitions': 'उपलब्ध वर्कफ़्लो',
'orchestration.noDefinitions': 'कोई वर्कफ़्लो परिभाषा उपलब्ध नहीं है।',
'orchestration.approvalRequired': 'अनुमोदन आवश्यक',
'orchestration.start': 'शुरू करें',
'orchestration.confirmStart': 'रन शुरू करें',
'orchestration.starting': 'शुरू हो रहा है…',
'orchestration.questionLabel': 'शोध प्रश्न (वैकल्पिक)',
'orchestration.questionPlaceholder': 'एजेंटों को क्या शोध करना चाहिए?',
'orchestration.runProgress': 'रन प्रगति',
'orchestration.recentRuns': 'हाल के रन',
'orchestration.noRuns': 'अभी तक कोई वर्कफ़्लो रन नहीं।',
'orchestration.close': 'बंद करें',
'orchestration.tier.readOnly': 'केवल-पढ़ने योग्य',
'orchestration.tier.standard': 'मानक',
'orchestration.tier.editCapable': 'संपादन-सक्षम',
'orchestration.approval.title': 'इस वर्कफ़्लो रन को अनुमोदित करें',
'orchestration.approval.body':
'यह एक उच्च-लागत या उच्च-समवर्ती रन है और शुरू होने से पहले इसे आपके स्पष्ट अनुमोदन की आवश्यकता है:',
'orchestration.approval.reason.tier': 'इसके एजेंट केवल-पढ़ने योग्य शोध से परे कार्य कर सकते हैं।',
'orchestration.approval.reason.concurrency': 'यह एक साथ कई एजेंट चलाता है।',
'orchestration.approval.reason.children':
'यह कुल मिलाकर बहुत बड़ी संख्या में एजेंट उत्पन्न कर सकता है।',
'orchestration.approval.tier': 'सुरक्षा स्तर',
'orchestration.approval.concurrency': 'समवर्तीता',
'orchestration.approval.maxChildren': 'अधिकतम एजेंट',
'orchestration.approval.approve': 'अनुमोदित करें और शुरू करें',
'orchestration.approval.starting': 'शुरू हो रहा है…',
'orchestration.approval.cancel': 'रद्द करें',
'orchestration.runStatus.pending': 'लंबित',
'orchestration.runStatus.running': 'चल रहा है',
'orchestration.runStatus.completed': 'पूर्ण',
'orchestration.runStatus.failed': 'विफल',
'orchestration.runStatus.cancelled': 'रद्द',
'orchestration.runStatus.interrupted': 'बाधित',
'orchestration.phaseStatus.pending': 'लंबित',
'orchestration.phaseStatus.running': 'चल रहा है',
'orchestration.phaseStatus.completed': 'पूर्ण',
'orchestration.phaseStatus.failed': 'विफल',
'orchestration.detail.stop': 'रोकें',
'orchestration.detail.resume': 'पुनः आरंभ करें',
'orchestration.detail.agents': 'एजेंट',
'orchestration.detail.childRefs': 'चाइल्ड एजेंट',
'orchestration.detail.synthesis': 'अंतिम संश्लेषण',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'एजेंट प्रोफ़ाइल',
'settings.profiles.subtitle': 'विशिष्ट एजेंट — हर एक की अपनी आत्मा, स्मृति, कनेक्टर और कौशल।',
+51
View File
@@ -5488,6 +5488,57 @@ const messages: TranslationMap = {
'notch.speaking': 'Berbicara…',
'notch.transcribing': 'Mentranskrip…',
'notch.executing': 'Mengeksekusi…',
'memory.tab.orchestration': 'Orkestrasi',
'memory.tab.orchestrationDescription':
'Jalankan alur kerja multi-agen — sebarkan satu pertanyaan ke agen-agen paralel, periksa silang temuan mereka, dan saksikan setiap fase berakhir menjadi satu jawaban tersintesis.',
'orchestration.subtitle':
'Mulai alur kerja multi-agen, pantau kemajuan fasenya, dan baca hasil tersintesisnya.',
'orchestration.loading': 'Memuat alur kerja…',
'orchestration.failedToLoad': 'Gagal memuat alur kerja',
'orchestration.definitions': 'Alur kerja tersedia',
'orchestration.noDefinitions': 'Tidak ada definisi alur kerja yang tersedia.',
'orchestration.approvalRequired': 'Persetujuan diperlukan',
'orchestration.start': 'Mulai',
'orchestration.confirmStart': 'Mulai jalankan',
'orchestration.starting': 'Memulai…',
'orchestration.questionLabel': 'Pertanyaan riset (opsional)',
'orchestration.questionPlaceholder': 'Apa yang harus diteliti para agen?',
'orchestration.runProgress': 'Kemajuan eksekusi',
'orchestration.recentRuns': 'Eksekusi terbaru',
'orchestration.noRuns': 'Belum ada eksekusi alur kerja.',
'orchestration.close': 'Tutup',
'orchestration.tier.readOnly': 'Hanya-baca',
'orchestration.tier.standard': 'Standar',
'orchestration.tier.editCapable': 'Mampu menyunting',
'orchestration.approval.title': 'Setujui eksekusi alur kerja ini',
'orchestration.approval.body':
'Ini adalah eksekusi berbiaya tinggi atau berkonkurensi tinggi dan memerlukan persetujuan eksplisit Anda sebelum dimulai:',
'orchestration.approval.reason.tier':
'Agen-agennya dapat melakukan tindakan di luar riset hanya-baca.',
'orchestration.approval.reason.concurrency': 'Ini menjalankan banyak agen pada saat yang sama.',
'orchestration.approval.reason.children':
'Ini dapat memunculkan agen dalam jumlah sangat besar secara total.',
'orchestration.approval.tier': 'Tingkat keamanan',
'orchestration.approval.concurrency': 'Konkurensi',
'orchestration.approval.maxChildren': 'Maks. agen',
'orchestration.approval.approve': 'Setujui & mulai',
'orchestration.approval.starting': 'Memulai…',
'orchestration.approval.cancel': 'Batal',
'orchestration.runStatus.pending': 'Menunggu',
'orchestration.runStatus.running': 'Berjalan',
'orchestration.runStatus.completed': 'Selesai',
'orchestration.runStatus.failed': 'Gagal',
'orchestration.runStatus.cancelled': 'Dibatalkan',
'orchestration.runStatus.interrupted': 'Terputus',
'orchestration.phaseStatus.pending': 'Menunggu',
'orchestration.phaseStatus.running': 'Berjalan',
'orchestration.phaseStatus.completed': 'Selesai',
'orchestration.phaseStatus.failed': 'Gagal',
'orchestration.detail.stop': 'Hentikan',
'orchestration.detail.resume': 'Lanjutkan',
'orchestration.detail.agents': 'agen',
'orchestration.detail.childRefs': 'Agen anak',
'orchestration.detail.synthesis': 'Sintesis akhir',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Profil Agen',
'settings.profiles.subtitle':
+50
View File
@@ -5574,6 +5574,56 @@ const messages: TranslationMap = {
'notch.speaking': 'Parlo…',
'notch.transcribing': 'Trascrizione…',
'notch.executing': 'Eseguendo…',
'memory.tab.orchestration': 'Orchestrazione',
'memory.tab.orchestrationDescription':
'Esegui flussi di lavoro multi-agente — distribuisci una domanda tra agenti paralleli, confronta i loro risultati e osserva ogni fase concludersi in ununica risposta sintetizzata.',
'orchestration.subtitle':
'Avvia un flusso di lavoro multi-agente, osserva lavanzamento delle sue fasi e leggi il risultato sintetizzato.',
'orchestration.loading': 'Caricamento dei flussi di lavoro…',
'orchestration.failedToLoad': 'Impossibile caricare i flussi di lavoro',
'orchestration.definitions': 'Flussi di lavoro disponibili',
'orchestration.noDefinitions': 'Nessuna definizione di flusso di lavoro disponibile.',
'orchestration.approvalRequired': 'Approvazione richiesta',
'orchestration.start': 'Avvia',
'orchestration.confirmStart': 'Avvia esecuzione',
'orchestration.starting': 'Avvio in corso…',
'orchestration.questionLabel': 'Domanda di ricerca (facoltativa)',
'orchestration.questionPlaceholder': 'Cosa devono ricercare gli agenti?',
'orchestration.runProgress': 'Avanzamento esecuzione',
'orchestration.recentRuns': 'Esecuzioni recenti',
'orchestration.noRuns': 'Ancora nessuna esecuzione di flussi di lavoro.',
'orchestration.close': 'Chiudi',
'orchestration.tier.readOnly': 'Sola lettura',
'orchestration.tier.standard': 'Standard',
'orchestration.tier.editCapable': 'Con capacità di modifica',
'orchestration.approval.title': 'Approva questa esecuzione del flusso di lavoro',
'orchestration.approval.body':
'Questa è unesecuzione ad alto costo o ad alta concorrenza e richiede la tua approvazione esplicita prima di iniziare:',
'orchestration.approval.reason.tier':
'I suoi agenti possono compiere azioni oltre la ricerca in sola lettura.',
'orchestration.approval.reason.concurrency': 'Esegue molti agenti contemporaneamente.',
'orchestration.approval.reason.children': 'Può generare un gran numero di agenti in totale.',
'orchestration.approval.tier': 'Livello di sicurezza',
'orchestration.approval.concurrency': 'Concorrenza',
'orchestration.approval.maxChildren': 'Numero massimo di agenti',
'orchestration.approval.approve': 'Approva e avvia',
'orchestration.approval.starting': 'Avvio in corso…',
'orchestration.approval.cancel': 'Annulla',
'orchestration.runStatus.pending': 'In attesa',
'orchestration.runStatus.running': 'In esecuzione',
'orchestration.runStatus.completed': 'Completata',
'orchestration.runStatus.failed': 'Non riuscita',
'orchestration.runStatus.cancelled': 'Annullata',
'orchestration.runStatus.interrupted': 'Interrotta',
'orchestration.phaseStatus.pending': 'In attesa',
'orchestration.phaseStatus.running': 'In esecuzione',
'orchestration.phaseStatus.completed': 'Completata',
'orchestration.phaseStatus.failed': 'Non riuscita',
'orchestration.detail.stop': 'Arresta',
'orchestration.detail.resume': 'Riprendi',
'orchestration.detail.agents': 'agenti',
'orchestration.detail.childRefs': 'Agenti figli',
'orchestration.detail.synthesis': 'Sintesi finale',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Profili agente',
'settings.profiles.subtitle':
+51
View File
@@ -5416,6 +5416,57 @@ const messages: TranslationMap = {
'notch.speaking': '말하는 중…',
'notch.transcribing': '변환 중…',
'notch.executing': '실행 중…',
'memory.tab.orchestration': '오케스트레이션',
'memory.tab.orchestrationDescription':
'다중 에이전트 워크플로를 실행하세요. 하나의 질문을 병렬 에이전트들에 분산하고, 그 결과를 교차 검증하며, 각 단계가 하나의 종합된 답변으로 완성되는 과정을 지켜보세요.',
'orchestration.subtitle':
'다중 에이전트 워크플로를 시작하고, 단계 진행 상황을 지켜보며, 종합된 결과를 확인하세요.',
'orchestration.loading': '워크플로 불러오는 중…',
'orchestration.failedToLoad': '워크플로를 불러오지 못했습니다',
'orchestration.definitions': '사용 가능한 워크플로',
'orchestration.noDefinitions': '사용 가능한 워크플로 정의가 없습니다.',
'orchestration.approvalRequired': '승인 필요',
'orchestration.start': '시작',
'orchestration.confirmStart': '실행 시작',
'orchestration.starting': '시작 중…',
'orchestration.questionLabel': '연구 질문 (선택 사항)',
'orchestration.questionPlaceholder': '에이전트가 무엇을 조사해야 하나요?',
'orchestration.runProgress': '실행 진행 상황',
'orchestration.recentRuns': '최근 실행',
'orchestration.noRuns': '아직 워크플로 실행이 없습니다.',
'orchestration.close': '닫기',
'orchestration.tier.readOnly': '읽기 전용',
'orchestration.tier.standard': '표준',
'orchestration.tier.editCapable': '편집 가능',
'orchestration.approval.title': '이 워크플로 실행 승인',
'orchestration.approval.body':
'이것은 고비용 또는 고동시성 실행으로, 시작하기 전에 명시적인 승인이 필요합니다:',
'orchestration.approval.reason.tier':
'해당 에이전트는 읽기 전용 조사를 넘어서는 작업을 수행할 수 있습니다.',
'orchestration.approval.reason.concurrency': '동시에 많은 에이전트를 실행합니다.',
'orchestration.approval.reason.children':
'전체적으로 매우 많은 수의 에이전트를 생성할 수 있습니다.',
'orchestration.approval.tier': '보안 등급',
'orchestration.approval.concurrency': '동시성',
'orchestration.approval.maxChildren': '최대 에이전트 수',
'orchestration.approval.approve': '승인 및 시작',
'orchestration.approval.starting': '시작 중…',
'orchestration.approval.cancel': '취소',
'orchestration.runStatus.pending': '대기 중',
'orchestration.runStatus.running': '실행 중',
'orchestration.runStatus.completed': '완료됨',
'orchestration.runStatus.failed': '실패함',
'orchestration.runStatus.cancelled': '취소됨',
'orchestration.runStatus.interrupted': '중단됨',
'orchestration.phaseStatus.pending': '대기 중',
'orchestration.phaseStatus.running': '실행 중',
'orchestration.phaseStatus.completed': '완료됨',
'orchestration.phaseStatus.failed': '실패함',
'orchestration.detail.stop': '중지',
'orchestration.detail.resume': '재개',
'orchestration.detail.agents': '에이전트',
'orchestration.detail.childRefs': '하위 에이전트',
'orchestration.detail.synthesis': '최종 종합',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': '에이전트 프로필',
'settings.profiles.subtitle':
+50
View File
@@ -5557,6 +5557,56 @@ const messages: TranslationMap = {
'notch.speaking': 'Mówię…',
'notch.transcribing': 'Transkrybuję…',
'notch.executing': 'Wykonuję…',
'memory.tab.orchestration': 'Orkiestracja',
'memory.tab.orchestrationDescription':
'Uruchamiaj przepływy pracy z wieloma agentami — rozdziel pytanie między równoległych agentów, porównaj ich ustalenia i obserwuj, jak każda faza kończy się jedną zsyntetyzowaną odpowiedzią.',
'orchestration.subtitle':
'Uruchom przepływ pracy z wieloma agentami, obserwuj postęp jego faz i przeczytaj zsyntetyzowany wynik.',
'orchestration.loading': 'Ładowanie przepływów pracy…',
'orchestration.failedToLoad': 'Nie udało się załadować przepływów pracy',
'orchestration.definitions': 'Dostępne przepływy pracy',
'orchestration.noDefinitions': 'Brak dostępnych definicji przepływów pracy.',
'orchestration.approvalRequired': 'Wymagane zatwierdzenie',
'orchestration.start': 'Uruchom',
'orchestration.confirmStart': 'Uruchom wykonanie',
'orchestration.starting': 'Uruchamianie…',
'orchestration.questionLabel': 'Pytanie badawcze (opcjonalne)',
'orchestration.questionPlaceholder': 'Co agenci mają zbadać?',
'orchestration.runProgress': 'Postęp wykonania',
'orchestration.recentRuns': 'Ostatnie wykonania',
'orchestration.noRuns': 'Brak wykonań przepływów pracy.',
'orchestration.close': 'Zamknij',
'orchestration.tier.readOnly': 'Tylko do odczytu',
'orchestration.tier.standard': 'Standardowy',
'orchestration.tier.editCapable': 'Z możliwością edycji',
'orchestration.approval.title': 'Zatwierdź to wykonanie przepływu pracy',
'orchestration.approval.body':
'To kosztowne lub silnie współbieżne wykonanie wymaga Twojego wyraźnego zatwierdzenia przed rozpoczęciem:',
'orchestration.approval.reason.tier':
'Jego agenci mogą wykonywać działania wykraczające poza badania tylko do odczytu.',
'orchestration.approval.reason.concurrency': 'Uruchamia wielu agentów jednocześnie.',
'orchestration.approval.reason.children': 'Może łącznie utworzyć bardzo dużą liczbę agentów.',
'orchestration.approval.tier': 'Poziom bezpieczeństwa',
'orchestration.approval.concurrency': 'Współbieżność',
'orchestration.approval.maxChildren': 'Maks. agentów',
'orchestration.approval.approve': 'Zatwierdź i uruchom',
'orchestration.approval.starting': 'Uruchamianie…',
'orchestration.approval.cancel': 'Anuluj',
'orchestration.runStatus.pending': 'Oczekujące',
'orchestration.runStatus.running': 'W trakcie',
'orchestration.runStatus.completed': 'Zakończone',
'orchestration.runStatus.failed': 'Nieudane',
'orchestration.runStatus.cancelled': 'Anulowane',
'orchestration.runStatus.interrupted': 'Przerwane',
'orchestration.phaseStatus.pending': 'Oczekująca',
'orchestration.phaseStatus.running': 'W trakcie',
'orchestration.phaseStatus.completed': 'Zakończona',
'orchestration.phaseStatus.failed': 'Nieudana',
'orchestration.detail.stop': 'Zatrzymaj',
'orchestration.detail.resume': 'Wznów',
'orchestration.detail.agents': 'agenci',
'orchestration.detail.childRefs': 'Agenci podrzędni',
'orchestration.detail.synthesis': 'Synteza końcowa',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Profile agenta',
'settings.profiles.subtitle':
+50
View File
@@ -5567,6 +5567,56 @@ const messages: TranslationMap = {
'notch.speaking': 'Falando…',
'notch.transcribing': 'Transcrevendo…',
'notch.executing': 'Executando…',
'memory.tab.orchestration': 'Orquestração',
'memory.tab.orchestrationDescription':
'Execute fluxos de trabalho com múltiplos agentes — distribua uma pergunta entre agentes paralelos, confronte seus resultados e veja cada fase culminar em uma única resposta sintetizada.',
'orchestration.subtitle':
'Inicie um fluxo de trabalho com múltiplos agentes, acompanhe o progresso de suas fases e leia o resultado sintetizado.',
'orchestration.loading': 'Carregando fluxos de trabalho…',
'orchestration.failedToLoad': 'Falha ao carregar os fluxos de trabalho',
'orchestration.definitions': 'Fluxos de trabalho disponíveis',
'orchestration.noDefinitions': 'Nenhuma definição de fluxo de trabalho disponível.',
'orchestration.approvalRequired': 'Aprovação necessária',
'orchestration.start': 'Iniciar',
'orchestration.confirmStart': 'Iniciar execução',
'orchestration.starting': 'Iniciando…',
'orchestration.questionLabel': 'Pergunta de pesquisa (opcional)',
'orchestration.questionPlaceholder': 'O que os agentes devem pesquisar?',
'orchestration.runProgress': 'Progresso da execução',
'orchestration.recentRuns': 'Execuções recentes',
'orchestration.noRuns': 'Ainda não há execuções de fluxos de trabalho.',
'orchestration.close': 'Fechar',
'orchestration.tier.readOnly': 'Somente leitura',
'orchestration.tier.standard': 'Padrão',
'orchestration.tier.editCapable': 'Com capacidade de edição',
'orchestration.approval.title': 'Aprovar esta execução do fluxo de trabalho',
'orchestration.approval.body':
'Esta é uma execução de alto custo ou alta concorrência e precisa da sua aprovação explícita antes de iniciar:',
'orchestration.approval.reason.tier':
'Seus agentes podem realizar ações além da pesquisa somente leitura.',
'orchestration.approval.reason.concurrency': 'Ela executa muitos agentes ao mesmo tempo.',
'orchestration.approval.reason.children': 'Ela pode gerar um grande número de agentes no total.',
'orchestration.approval.tier': 'Nível de segurança',
'orchestration.approval.concurrency': 'Concorrência',
'orchestration.approval.maxChildren': 'Máx. de agentes',
'orchestration.approval.approve': 'Aprovar e iniciar',
'orchestration.approval.starting': 'Iniciando…',
'orchestration.approval.cancel': 'Cancelar',
'orchestration.runStatus.pending': 'Pendente',
'orchestration.runStatus.running': 'Em execução',
'orchestration.runStatus.completed': 'Concluída',
'orchestration.runStatus.failed': 'Falhou',
'orchestration.runStatus.cancelled': 'Cancelada',
'orchestration.runStatus.interrupted': 'Interrompida',
'orchestration.phaseStatus.pending': 'Pendente',
'orchestration.phaseStatus.running': 'Em execução',
'orchestration.phaseStatus.completed': 'Concluída',
'orchestration.phaseStatus.failed': 'Falhou',
'orchestration.detail.stop': 'Parar',
'orchestration.detail.resume': 'Retomar',
'orchestration.detail.agents': 'agentes',
'orchestration.detail.childRefs': 'Agentes filhos',
'orchestration.detail.synthesis': 'Síntese final',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Perfis de agente',
'settings.profiles.subtitle':
+51
View File
@@ -5527,6 +5527,57 @@ const messages: TranslationMap = {
'notch.speaking': 'Говорю…',
'notch.transcribing': 'Транскрибирую…',
'notch.executing': 'Выполняю…',
'memory.tab.orchestration': 'Оркестрация',
'memory.tab.orchestrationDescription':
'Запускайте мультиагентные рабочие процессы — распределяйте вопрос между параллельными агентами, перепроверяйте их выводы и наблюдайте, как каждая фаза сходится в единый синтезированный ответ.',
'orchestration.subtitle':
'Запустите мультиагентный рабочий процесс, следите за ходом его фаз и читайте синтезированный результат.',
'orchestration.loading': 'Загрузка рабочих процессов…',
'orchestration.failedToLoad': 'Не удалось загрузить рабочие процессы',
'orchestration.definitions': 'Доступные рабочие процессы',
'orchestration.noDefinitions': 'Нет доступных определений рабочих процессов.',
'orchestration.approvalRequired': 'Требуется подтверждение',
'orchestration.start': 'Запустить',
'orchestration.confirmStart': 'Запустить выполнение',
'orchestration.starting': 'Запуск…',
'orchestration.questionLabel': 'Исследовательский вопрос (необязательно)',
'orchestration.questionPlaceholder': 'Что должны исследовать агенты?',
'orchestration.runProgress': 'Ход выполнения',
'orchestration.recentRuns': 'Недавние выполнения',
'orchestration.noRuns': 'Пока нет выполнений рабочих процессов.',
'orchestration.close': 'Закрыть',
'orchestration.tier.readOnly': 'Только чтение',
'orchestration.tier.standard': 'Стандартный',
'orchestration.tier.editCapable': 'С правом изменения',
'orchestration.approval.title': 'Подтвердить это выполнение рабочего процесса',
'orchestration.approval.body':
'Это дорогостоящее или высокопараллельное выполнение, требующее вашего явного подтверждения перед запуском:',
'orchestration.approval.reason.tier':
'Его агенты могут выполнять действия за пределами исследования только для чтения.',
'orchestration.approval.reason.concurrency': 'Он одновременно запускает множество агентов.',
'orchestration.approval.reason.children':
'Он может в совокупности порождать очень большое число агентов.',
'orchestration.approval.tier': 'Уровень безопасности',
'orchestration.approval.concurrency': 'Параллелизм',
'orchestration.approval.maxChildren': 'Макс. агентов',
'orchestration.approval.approve': 'Подтвердить и запустить',
'orchestration.approval.starting': 'Запуск…',
'orchestration.approval.cancel': 'Отмена',
'orchestration.runStatus.pending': 'В ожидании',
'orchestration.runStatus.running': 'Выполняется',
'orchestration.runStatus.completed': 'Завершено',
'orchestration.runStatus.failed': 'Не удалось',
'orchestration.runStatus.cancelled': 'Отменено',
'orchestration.runStatus.interrupted': 'Прервано',
'orchestration.phaseStatus.pending': 'В ожидании',
'orchestration.phaseStatus.running': 'Выполняется',
'orchestration.phaseStatus.completed': 'Завершена',
'orchestration.phaseStatus.failed': 'Не удалась',
'orchestration.detail.stop': 'Остановить',
'orchestration.detail.resume': 'Возобновить',
'orchestration.detail.agents': 'агенты',
'orchestration.detail.childRefs': 'Дочерние агенты',
'orchestration.detail.synthesis': 'Итоговый синтез',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': 'Профили агента',
'settings.profiles.subtitle':
+47
View File
@@ -5194,6 +5194,53 @@ const messages: TranslationMap = {
'notch.speaking': '说话中…',
'notch.transcribing': '转录中…',
'notch.executing': '执行中…',
'memory.tab.orchestration': '编排',
'memory.tab.orchestrationDescription':
'运行多智能体工作流——将一个问题分发给多个并行智能体,交叉核对它们的发现,并观察每个阶段汇聚成一个综合答案。',
'orchestration.subtitle': '启动多智能体工作流,观察其各阶段进展,并阅读综合结果。',
'orchestration.loading': '正在加载工作流…',
'orchestration.failedToLoad': '加载工作流失败',
'orchestration.definitions': '可用工作流',
'orchestration.noDefinitions': '没有可用的工作流定义。',
'orchestration.approvalRequired': '需要批准',
'orchestration.start': '启动',
'orchestration.confirmStart': '启动运行',
'orchestration.starting': '正在启动…',
'orchestration.questionLabel': '研究问题(可选)',
'orchestration.questionPlaceholder': '智能体应研究什么?',
'orchestration.runProgress': '运行进度',
'orchestration.recentRuns': '近期运行',
'orchestration.noRuns': '尚无工作流运行。',
'orchestration.close': '关闭',
'orchestration.tier.readOnly': '只读',
'orchestration.tier.standard': '标准',
'orchestration.tier.editCapable': '可编辑',
'orchestration.approval.title': '批准此工作流运行',
'orchestration.approval.body': '这是一次高成本或高并发的运行,启动前需要您明确批准:',
'orchestration.approval.reason.tier': '其智能体可执行超出只读研究范围的操作。',
'orchestration.approval.reason.concurrency': '它会同时运行许多智能体。',
'orchestration.approval.reason.children': '它总共可能生成大量智能体。',
'orchestration.approval.tier': '安全级别',
'orchestration.approval.concurrency': '并发数',
'orchestration.approval.maxChildren': '最大智能体数',
'orchestration.approval.approve': '批准并启动',
'orchestration.approval.starting': '正在启动…',
'orchestration.approval.cancel': '取消',
'orchestration.runStatus.pending': '等待中',
'orchestration.runStatus.running': '运行中',
'orchestration.runStatus.completed': '已完成',
'orchestration.runStatus.failed': '已失败',
'orchestration.runStatus.cancelled': '已取消',
'orchestration.runStatus.interrupted': '已中断',
'orchestration.phaseStatus.pending': '等待中',
'orchestration.phaseStatus.running': '运行中',
'orchestration.phaseStatus.completed': '已完成',
'orchestration.phaseStatus.failed': '已失败',
'orchestration.detail.stop': '停止',
'orchestration.detail.resume': '继续',
'orchestration.detail.agents': '个智能体',
'orchestration.detail.childRefs': '子智能体',
'orchestration.detail.synthesis': '最终综合',
// ── Agent Profiles ───────────────────────────────────────────────────────
'settings.profiles.title': '智能体配置',
'settings.profiles.subtitle': '风格化智能体——每个都有自己的灵魂、记忆、连接器和技能。',
+10
View File
@@ -5,6 +5,7 @@ import { useSearchParams } from 'react-router-dom';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
import IntelligenceAgentsTab from '../components/intelligence/IntelligenceAgentsTab';
import IntelligenceAgentWorkTab from '../components/intelligence/IntelligenceAgentWorkTab';
import IntelligenceOrchestrationTab from '../components/intelligence/IntelligenceOrchestrationTab';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab';
import IntelligenceTeamsTab from '../components/intelligence/IntelligenceTeamsTab';
@@ -37,6 +38,7 @@ type IntelligenceTab =
| 'teams'
| 'agents'
| 'workflows'
| 'orchestration'
| 'council';
const INTELLIGENCE_TABS: IntelligenceTab[] = [
@@ -47,6 +49,7 @@ const INTELLIGENCE_TABS: IntelligenceTab[] = [
'teams',
'agents',
'workflows',
'orchestration',
'council',
];
@@ -178,6 +181,11 @@ export default function Intelligence({ tabParamKey = 'tab' }: IntelligenceProps
label: t('memory.tab.workflows'),
description: t('memory.tab.workflowsDescription'),
},
{
id: 'orchestration',
label: t('memory.tab.orchestration'),
description: t('memory.tab.orchestrationDescription'),
},
{ id: 'council', label: t('memory.tab.council'), devOnly: true },
{ id: 'agents', label: t('memory.tab.agents'), description: t('memory.tab.agentsDescription') },
];
@@ -273,6 +281,8 @@ export default function Intelligence({ tabParamKey = 'tab' }: IntelligenceProps
{activeTab === 'workflows' && <WorkflowsTab />}
{activeTab === 'orchestration' && <IntelligenceOrchestrationTab />}
{activeTab === 'council' && <ModelCouncilTab />}
</div>
</div>
@@ -0,0 +1,166 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { callCoreRpc } from '../coreRpcClient';
import {
assessWorkflowCost,
type WorkflowDefinition,
type WorkflowRun,
workflowRunsApi,
} from './workflowRunsApi';
vi.mock('../coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
const mockRpc = vi.mocked(callCoreRpc);
function def(overrides: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
return {
id: 'parallel_research_cross_check',
name: 'Parallel research with cross-checking',
description: 'Decompose, research in parallel, cross-check, synthesize.',
phases: [
{ name: 'decompose', description: '', agentIds: ['planner'], dependsOn: [] },
{
name: 'research',
description: '',
agentIds: ['researcher', 'researcher'],
dependsOn: ['decompose'],
},
],
defaultConcurrency: 2,
maxChildren: 8,
safetyTier: 'read_only',
...overrides,
};
}
function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
return {
id: 'wfrun-1',
definitionId: 'parallel_research_cross_check',
parentThreadId: null,
input: { question: 'why is the sky blue?' },
phaseStates: {},
childRunIds: [],
status: 'running',
summary: null,
startedAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
completedAt: null,
...overrides,
};
}
describe('assessWorkflowCost', () => {
it('requires approval for the builtin (maxChildren >= 8)', () => {
const result = assessWorkflowCost(def({ maxChildren: 8, defaultConcurrency: 2 }));
expect(result.requiresApproval).toBe(true);
expect(result.reasons).toContain('high_children');
});
it('does not require approval for a small read-only fan-out', () => {
const result = assessWorkflowCost(
def({ maxChildren: 4, defaultConcurrency: 2, safetyTier: 'read_only' })
);
expect(result.requiresApproval).toBe(false);
expect(result.reasons).toHaveLength(0);
});
it('flags non-read-only tiers', () => {
const result = assessWorkflowCost(
def({ maxChildren: 2, defaultConcurrency: 1, safetyTier: 'edit_capable' })
);
expect(result.requiresApproval).toBe(true);
expect(result.reasons).toContain('non_read_only_tier');
});
it('flags high concurrency', () => {
const result = assessWorkflowCost(
def({ maxChildren: 2, defaultConcurrency: 4, safetyTier: 'read_only' })
);
expect(result.requiresApproval).toBe(true);
expect(result.reasons).toContain('high_concurrency');
});
});
describe('workflowRunsApi', () => {
beforeEach(() => mockRpc.mockReset());
it('listDefinitions calls the RPC and returns the array', async () => {
mockRpc.mockResolvedValueOnce({ definitions: [def()], count: 1 });
const defs = await workflowRunsApi.listDefinitions();
expect(mockRpc).toHaveBeenCalledWith({ method: 'openhuman.workflow_run_list_definitions' });
expect(defs).toHaveLength(1);
expect(defs[0].id).toBe('parallel_research_cross_check');
});
it('listDefinitions tolerates a missing array', async () => {
mockRpc.mockResolvedValueOnce({});
expect(await workflowRunsApi.listDefinitions()).toEqual([]);
});
it('listRuns forwards filters and returns the runs array', async () => {
mockRpc.mockResolvedValueOnce({ runs: [run()], count: 1 });
const runs = await workflowRunsApi.listRuns({ limit: 50, status: 'running' });
expect(mockRpc).toHaveBeenCalledWith({
method: 'openhuman.workflow_run_list',
params: { limit: 50, status: 'running' },
});
expect(runs).toHaveLength(1);
expect(runs[0].id).toBe('wfrun-1');
});
it('listRuns defaults params and tolerates a missing array', async () => {
mockRpc.mockResolvedValueOnce({});
const runs = await workflowRunsApi.listRuns();
expect(mockRpc).toHaveBeenCalledWith({ method: 'openhuman.workflow_run_list', params: {} });
expect(runs).toEqual([]);
});
it('getRun returns the run when present', async () => {
mockRpc.mockResolvedValueOnce({ workflowRun: run() });
const fetched = await workflowRunsApi.getRun('wfrun-1');
expect(mockRpc).toHaveBeenCalledWith({
method: 'openhuman.workflow_run_get',
params: { id: 'wfrun-1' },
});
expect(fetched?.id).toBe('wfrun-1');
});
it('startRun forwards definitionId + input and returns the run', async () => {
mockRpc.mockResolvedValueOnce({ workflowRun: run() });
const started = await workflowRunsApi.startRun({
definitionId: 'parallel_research_cross_check',
input: { question: 'q' },
});
expect(mockRpc).toHaveBeenCalledWith({
method: 'openhuman.workflow_run_start',
params: { definitionId: 'parallel_research_cross_check', input: { question: 'q' } },
});
expect(started.id).toBe('wfrun-1');
});
it('getRun returns null when the run is absent', async () => {
mockRpc.mockResolvedValueOnce({ workflowRun: null });
expect(await workflowRunsApi.getRun('nope')).toBeNull();
});
it('stopRun returns the updated run', async () => {
mockRpc.mockResolvedValueOnce({ workflowRun: run({ status: 'interrupted' }) });
const stopped = await workflowRunsApi.stopRun('wfrun-1');
expect(mockRpc).toHaveBeenCalledWith({
method: 'openhuman.workflow_run_stop',
params: { id: 'wfrun-1' },
});
expect(stopped?.status).toBe('interrupted');
});
it('resumeRun returns the resumed run', async () => {
mockRpc.mockResolvedValueOnce({ workflowRun: run({ status: 'running' }) });
const resumed = await workflowRunsApi.resumeRun('wfrun-1');
expect(mockRpc).toHaveBeenCalledWith({
method: 'openhuman.workflow_run_resume',
params: { id: 'wfrun-1' },
});
expect(resumed.status).toBe('running');
});
});
+260
View File
@@ -0,0 +1,260 @@
/**
* Frontend client for the declarative workflow-runs engine (#3375).
*
* Wraps the six `openhuman.workflow_run_*` JSON-RPC controllers exposed by the
* Rust `workflow_runs` domain:
* - `workflow_run_list_definitions` — catalog of runnable workflow definitions
* - `workflow_run_list` — durable runs (with filters / paging)
* - `workflow_run_get` — single run (poll target for progress)
* - `workflow_run_start` — start a run from a definition id
* - `workflow_run_stop` — interrupt a running workflow
* - `workflow_run_resume` — resume an interrupted workflow
*
* The Rust controllers serialize structs with `rename_all = "camelCase"` and
* the status / safety-tier enums with `rename_all = "snake_case"`, so the wire
* payload already matches these TypeScript shapes — no snake/camel transform is
* needed. Progress is poll-based in this engine (no socket events yet): start a
* run, then poll `getRun(id)` until `status` is terminal.
*/
import debug from 'debug';
import { callCoreRpc } from '../coreRpcClient';
const log = debug('workflowRunsApi');
// ---------------------------------------------------------------------------
// Wire types — mirror `src/openhuman/workflow_runs/types.rs`.
// ---------------------------------------------------------------------------
/** What a workflow's child agents are permitted to do. (`snake_case` on wire.) */
export type WorkflowSafetyTier = 'read_only' | 'standard' | 'edit_capable';
/** Lifecycle status of a durable run. (`snake_case` on wire.) */
export type WorkflowRunStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'cancelled'
| 'interrupted';
/** Per-phase progress status inside `WorkflowRun.phaseStates`. */
export type WorkflowPhaseStatus = 'pending' | 'running' | 'completed' | 'failed';
/** One ordered phase of a workflow definition. */
export interface WorkflowPhase {
/** Unique within the definition (e.g. "decompose", "research"). */
name: string;
/** Human-readable purpose. */
description: string;
/** Agent definition ids fanned out (in parallel) for this phase. */
agentIds: string[];
/** Phase names that must complete before this one runs. */
dependsOn: string[];
}
/** A runnable workflow definition (builtin or, later, user-authored). */
export interface WorkflowDefinition {
id: string;
name: string;
description: string;
phases: WorkflowPhase[];
/** Max agents run concurrently within a single phase. */
defaultConcurrency: number;
/** Hard cap on child agents spawned across the whole run. */
maxChildren: number;
safetyTier: WorkflowSafetyTier;
}
/** One child agent's result within a phase. */
export interface WorkflowPhaseOutput {
/** Orchestration id of the spawned child agent. */
orchestrationId: string;
/** Agent definition id (e.g. "researcher"). */
agentId: string;
/** The child's result summary. */
output: string;
}
/** Progress record for a single phase, keyed by phase name in `phaseStates`. */
export interface WorkflowPhaseState {
status: WorkflowPhaseStatus;
outputs: WorkflowPhaseOutput[];
/** Present only when `status === 'failed'`. */
reason?: string;
}
/** A durable workflow run. Mirrors the Rust `WorkflowRun`. */
export interface WorkflowRun {
id: string;
definitionId: string;
parentThreadId: string | null;
/** Run input object (e.g. `{ question: "..." }`). */
input: unknown;
/** Per-phase progress, keyed by phase name. */
phaseStates: Record<string, WorkflowPhaseState>;
/** Orchestration ids of every spawned child agent. */
childRunIds: string[];
status: WorkflowRunStatus;
/** Final synthesized output — present once `status === 'completed'`. */
summary: string | null;
startedAt: string;
updatedAt: string;
completedAt: string | null;
}
interface ListDefinitionsResult {
definitions: WorkflowDefinition[];
count: number;
}
interface ListRunsResult {
runs: WorkflowRun[];
count: number;
}
interface RunResult {
workflowRun: WorkflowRun;
}
interface MaybeRunResult {
workflowRun: WorkflowRun | null;
}
/** Filters accepted by `workflow_run_list`. */
export interface ListRunsParams {
definitionId?: string;
status?: WorkflowRunStatus;
parentThreadId?: string;
limit?: number;
offset?: number;
}
/** Parameters accepted by `workflow_run_start`. */
export interface StartRunParams {
definitionId: string;
/** Run input, e.g. `{ question: "..." }` or `{ modelOverride: "..." }`. */
input?: Record<string, unknown>;
/** Originating thread id, for lineage. */
parentThreadId?: string;
}
// ---------------------------------------------------------------------------
// Cost / concurrency safety gate (#3375 AC: high-cost runs require approval).
// ---------------------------------------------------------------------------
/**
* Why a run is considered high-cost / high-concurrency and therefore gated
* behind explicit approval. Stable codes — the UI maps each to a localized
* sentence so the user understands what they're approving.
*/
export type WorkflowCostReason = 'non_read_only_tier' | 'high_concurrency' | 'high_children';
export interface WorkflowCostAssessment {
/** True when the definition must be explicitly approved before starting. */
requiresApproval: boolean;
/** Machine-readable reasons (empty when no approval required). */
reasons: WorkflowCostReason[];
}
/**
* Per-phase concurrency at or above this is treated as high-concurrency.
* The builtin parallel-research workflow runs at 2, so routine read-only
* fan-outs stay frictionless; only unusually wide phases trip the gate.
*/
export const HIGH_CONCURRENCY_THRESHOLD = 4;
/**
* Total child-agent budget at or above this is treated as high-cost — every
* child is a full agent turn (tokens + wall-clock), so a large cap is the
* clearest proxy for spend in the absence of an explicit cost field.
*/
export const HIGH_CHILDREN_THRESHOLD = 8;
/**
* Decide whether starting `def` needs explicit user approval.
*
* The Rust safety tiers exist on the definition but the engine does not yet
* gate on them, so the approval decision lives here on the client: any
* non-`read_only` tier (child agents may take actions / edit files), or a
* wide/expensive fan-out, requires the user to confirm before we call
* `startRun`. Read-only, low-fan-out definitions start immediately.
*/
export function assessWorkflowCost(def: WorkflowDefinition): WorkflowCostAssessment {
const reasons: WorkflowCostReason[] = [];
if (def.safetyTier !== 'read_only') reasons.push('non_read_only_tier');
if (def.defaultConcurrency >= HIGH_CONCURRENCY_THRESHOLD) reasons.push('high_concurrency');
if (def.maxChildren >= HIGH_CHILDREN_THRESHOLD) reasons.push('high_children');
return { requiresApproval: reasons.length > 0, reasons };
}
// ---------------------------------------------------------------------------
// RPC client.
// ---------------------------------------------------------------------------
export const workflowRunsApi = {
/** List available declarative workflow definitions (builtins, currently). */
listDefinitions: async (): Promise<WorkflowDefinition[]> => {
log('listDefinitions: request');
const result = await callCoreRpc<ListDefinitionsResult>({
method: 'openhuman.workflow_run_list_definitions',
});
const definitions = result?.definitions ?? [];
log('listDefinitions: count=%d', definitions.length);
return definitions;
},
/** List durable workflow runs, newest first, with optional filters. */
listRuns: async (params: ListRunsParams = {}): Promise<WorkflowRun[]> => {
log('listRuns: request %o', params);
const result = await callCoreRpc<ListRunsResult>({
method: 'openhuman.workflow_run_list',
params,
});
const runs = result?.runs ?? [];
log('listRuns: count=%d', runs.length);
return runs;
},
/** Fetch a single run by id. Returns null when no such run exists. */
getRun: async (id: string): Promise<WorkflowRun | null> => {
log('getRun: request id=%s', id);
const result = await callCoreRpc<MaybeRunResult>({
method: 'openhuman.workflow_run_get',
params: { id },
});
const run = result?.workflowRun ?? null;
log('getRun: status=%s', run?.status ?? 'null');
return run;
},
/** Start a run from a definition id; returns the created Running run. */
startRun: async (params: StartRunParams): Promise<WorkflowRun> => {
log('startRun: request definitionId=%s', params.definitionId);
const result = await callCoreRpc<RunResult>({ method: 'openhuman.workflow_run_start', params });
log('startRun: id=%s status=%s', result.workflowRun.id, result.workflowRun.status);
return result.workflowRun;
},
/** Stop a running workflow after its current phase. Null if id unknown. */
stopRun: async (id: string): Promise<WorkflowRun | null> => {
log('stopRun: request id=%s', id);
const result = await callCoreRpc<MaybeRunResult>({
method: 'openhuman.workflow_run_stop',
params: { id },
});
log('stopRun: status=%s', result?.workflowRun?.status ?? 'null');
return result?.workflowRun ?? null;
},
/** Resume an interrupted workflow from the first incomplete phase. */
resumeRun: async (id: string): Promise<WorkflowRun> => {
log('resumeRun: request id=%s', id);
const result = await callCoreRpc<RunResult>({
method: 'openhuman.workflow_run_resume',
params: { id },
});
log('resumeRun: status=%s', result.workflowRun.status);
return result.workflowRun;
},
};
+10
View File
@@ -503,6 +503,16 @@ pub(super) const CAPABILITIES: &[Capability] = &[
status: CapabilityStatus::Beta,
privacy: DERIVED_TO_BACKEND,
},
Capability {
id: "intelligence.workflow_orchestration",
name: "Workflow Orchestration",
domain: "workflow_runs",
category: CapabilityCategory::Intelligence,
description: "Run declarative multi-agent workflows such as parallel research with cross-checking: a question is decomposed into angles, researched in parallel, adversarially cross-checked, and synthesized into one cited report. Watch each phase progress with its child agent results, stop or resume a run, and read the final synthesis. High-cost / high-concurrency runs require explicit approval before starting.",
how_to: "Intelligence > Orchestration > pick a workflow and Start",
status: CapabilityStatus::Beta,
privacy: DERIVED_TO_BACKEND,
},
Capability {
id: "intelligence.agent_library",
name: "Agents Library",