diff --git a/app/src/components/intelligence/IntelligenceAgentWorkTab.test.tsx b/app/src/components/intelligence/IntelligenceAgentWorkTab.test.tsx new file mode 100644 index 000000000..cd82ba0d2 --- /dev/null +++ b/app/src/components/intelligence/IntelligenceAgentWorkTab.test.tsx @@ -0,0 +1,117 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { agentWorkApi, type AgentWorkResponse } from '../../services/api/agentWorkApi'; +import IntelligenceAgentWorkTab from './IntelligenceAgentWorkTab'; + +vi.mock('../../services/api/agentWorkApi', () => ({ agentWorkApi: { list: vi.fn() } })); + +// i18n → echo the key so assertions can target stable strings. +vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); + +// Navigation + store: the tab only dispatches + navigates on click; stub them. +vi.mock('react-router-dom', () => ({ useNavigate: () => vi.fn() })); +vi.mock('../../store/hooks', () => ({ useAppDispatch: () => vi.fn() })); +vi.mock('../../store/threadSlice', () => ({ + loadThreadMessages: vi.fn(), + loadThreads: vi.fn(), + setSelectedThread: vi.fn(), +})); + +const mockList = vi.mocked(agentWorkApi.list); + +function emptyResponse(): AgentWorkResponse { + return { + total: 0, + groups: (['needs_input', 'working', 'completed', 'failed', 'stopped'] as const).map(bucket => ({ + bucket, + count: 0, + rows: [], + })), + }; +} + +function workingResponse(): AgentWorkResponse { + return { + total: 1, + groups: [ + { bucket: 'needs_input', count: 0, rows: [] }, + { + bucket: 'working', + count: 1, + rows: [ + { + runId: 'run-1', + kind: 'subagent', + agentId: 'agent-a', + displayName: 'Researcher', + bucket: 'working', + status: 'running', + workerThreadId: 'thread-w', + startedAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:01:00Z', + elapsedMs: 60000, + inputTokens: 1200, + outputTokens: 300, + costUsd: 0.05, + toolCount: 3, + }, + ], + }, + { bucket: 'completed', count: 0, rows: [] }, + { bucket: 'failed', count: 0, rows: [] }, + { bucket: 'stopped', count: 0, rows: [] }, + ], + }; +} + +describe('IntelligenceAgentWorkTab', () => { + beforeEach(() => { + // Reset the queue + implementation so a prior test's resolve/reject can't + // leak via the mount setTimeout into the next render (clearMocks only wipes + // call history, not queued *Once values / persistent implementations). + mockList.mockReset(); + }); + + it('fetches agent work on mount', async () => { + mockList.mockResolvedValue(emptyResponse()); + render(); + await waitFor(() => expect(mockList).toHaveBeenCalledTimes(1)); + }); + + it('shows the loading state before the RPC resolves', () => { + mockList.mockReturnValue(new Promise(() => {})); + render(); + expect(screen.getByText('intelligence.agentWork.loading')).toBeInTheDocument(); + }); + + it('shows the error box when the RPC rejects', async () => { + mockList.mockRejectedValue(new Error('boom')); + render(); + await waitFor(() => + expect(screen.getByText(/intelligence\.agentWork\.failedToLoad/)).toBeInTheDocument() + ); + expect(screen.getByText(/boom/)).toBeInTheDocument(); + }); + + it('shows the empty state when total is 0', async () => { + mockList.mockResolvedValue(emptyResponse()); + render(); + await waitFor(() => + expect(screen.getByText('intelligence.agentWork.empty')).toBeInTheDocument() + ); + }); + + it('renders a grouped working row with its display name and bucket label', async () => { + mockList.mockResolvedValue(workingResponse()); + render(); + await waitFor(() => expect(screen.getByText('Researcher')).toBeInTheDocument()); + expect(screen.getByText('intelligence.agentWork.bucket.working')).toBeInTheDocument(); + // 1200 + 300 input/output tokens → "1.5K" + expect(screen.getByText('1.5K')).toBeInTheDocument(); + // $0.05 cost formatted + expect(screen.getByText('$0.05')).toBeInTheDocument(); + // worker-thread jump button present + expect(screen.getByText('intelligence.agentWork.openWorker')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/intelligence/IntelligenceAgentWorkTab.tsx b/app/src/components/intelligence/IntelligenceAgentWorkTab.tsx new file mode 100644 index 000000000..40aca95eb --- /dev/null +++ b/app/src/components/intelligence/IntelligenceAgentWorkTab.tsx @@ -0,0 +1,310 @@ +/** + * IntelligenceAgentWorkTab — the Background Agent Command Center. + * + * Reads `openhuman.agent_work_list` (via {@link agentWorkApi}) once on mount + * and renders every tracked background agent run, grouped into five lifecycle + * buckets in a fixed display order: needs_input → working → completed → + * failed → stopped. The handler always returns all five groups, so the order + * here is authoritative and never needs sorting. + * + * This tab is read-only and holds no Redux slice — it owns its own + * {data, loading, error} state, mirroring {@link IntelligenceTasksTab}'s + * mount pattern (mountedRef + a 0ms `setTimeout` so the first paint shows the + * loading state before the RPC resolves). Each row offers jumps to the parent + * / worker thread via the same navigate('/chat', { openThreadId }) path the + * Tasks tab uses for "View session". + */ +import debug from 'debug'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useT } from '../../lib/i18n/I18nContext'; +import { + agentWorkApi, + type AgentWorkBucket, + type AgentWorkResponse, + type AgentWorkRow, +} from '../../services/api/agentWorkApi'; +import { useAppDispatch } from '../../store/hooks'; +import { loadThreadMessages, loadThreads, setSelectedThread } from '../../store/threadSlice'; + +const log = debug('intelligence:agent-work'); + +/** Fixed display order of buckets — matches the handler's group order. */ +const BUCKET_ORDER: AgentWorkBucket[] = [ + 'needs_input', + 'working', + 'completed', + 'failed', + 'stopped', +]; + +/** Per-bucket accent classes (semantic palette from tailwind.config.js). */ +const BUCKET_ACCENT: Record = { + needs_input: + 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300', + working: + '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', + stopped: + 'border-stone-200 bg-stone-50 text-stone-600 dark:border-neutral-700 dark:bg-neutral-800/60 dark:text-neutral-300', +}; + +/** i18n key for each bucket's localized label. */ +const BUCKET_LABEL_KEY: Record = { + needs_input: 'intelligence.agentWork.bucket.needsInput', + working: 'intelligence.agentWork.bucket.working', + completed: 'intelligence.agentWork.bucket.completed', + failed: 'intelligence.agentWork.bucket.failed', + stopped: 'intelligence.agentWork.bucket.stopped', +}; + +/** i18n key for each granular run status (mirrors Rust `AgentRunStatus`). */ +const STATUS_LABEL_KEY: Record = { + pending: 'intelligence.agentWork.status.pending', + running: 'intelligence.agentWork.status.running', + awaiting_user: 'intelligence.agentWork.status.awaitingUser', + paused: 'intelligence.agentWork.status.paused', + completed: 'intelligence.agentWork.status.completed', + failed: 'intelligence.agentWork.status.failed', + cancelled: 'intelligence.agentWork.status.cancelled', + interrupted: 'intelligence.agentWork.status.interrupted', +}; + +/** i18n key for each run kind (mirrors Rust `AgentRunKind`). */ +const KIND_LABEL_KEY: Record = { + subagent: 'intelligence.agentWork.kind.subagent', + worker_thread: 'intelligence.agentWork.kind.workerThread', + background_agent: 'intelligence.agentWork.kind.backgroundAgent', + team_member: 'intelligence.agentWork.kind.teamMember', + workflow_child: 'intelligence.agentWork.kind.workflowChild', +}; + +/** Format an elapsed millisecond span as a compact "1h 23m" / "45s" string. */ +export function formatElapsed(ms: number | undefined): string { + if (ms === undefined || !Number.isFinite(ms) || ms < 0) return '—'; + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if (hours > 0) return `${hours}h ${minutes}m`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; +} + +/** Human-readable token count: 1234 → "1.2K", 2_500_000 → "2.5M". */ +export function formatTokens(value: number): string { + if (!Number.isFinite(value) || value < 0) return '0'; + if (value < 1000) return String(value); + if (value < 1_000_000) return `${(value / 1000).toFixed(1)}K`; + return `${(value / 1_000_000).toFixed(1)}M`; +} + +/** Format a USD cost with two decimals, e.g. 0.0123 → "$0.01". */ +export function formatCost(value: number): string { + if (!Number.isFinite(value) || value < 0) return '$0.00'; + return `$${value.toFixed(2)}`; +} + +export default function IntelligenceAgentWorkTab() { + const { t } = useT(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const mountedRef = useRef(true); + + const fetchWork = useCallback(async () => { + log('fetchWork: entry'); + setError(null); + try { + const response = await agentWorkApi.list(); + if (mountedRef.current) { + setData(response); + log('fetchWork: done total=%d', response.total); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + log('fetchWork: error %s', msg); + if (mountedRef.current) setError(msg); + } finally { + if (mountedRef.current) setLoading(false); + } + }, []); + + useEffect(() => { + mountedRef.current = true; + const handle = window.setTimeout(() => { + void fetchWork(); + }, 0); + return () => { + window.clearTimeout(handle); + mountedRef.current = false; + }; + }, [fetchWork]); + + // Open a thread in /chat — mirrors IntelligenceTasksTab's "View session" + // path so the chat surface lands on the requested thread instead of just + // the Conversations list. Navigation only; the thread is not marked active. + const openThread = useCallback( + (threadId: string) => { + log('openThread threadId=%s', threadId); + dispatch(setSelectedThread(threadId)); + void dispatch(loadThreads()); + void dispatch(loadThreadMessages(threadId)); + navigate('/chat', { state: { openThreadId: threadId } }); + }, + [dispatch, navigate] + ); + + if (loading) { + return ( +
+
+ {t('intelligence.agentWork.loading')} +
+ ); + } + + if (error) { + return ( +
+ {t('intelligence.agentWork.failedToLoad')}: {error} +
+ ); + } + + if (!data || data.total === 0) { + return ( +
+

+ {t('intelligence.agentWork.subtitle')} +

+
+ {t('intelligence.agentWork.empty')} +
+
+ ); + } + + const groupByBucket = new Map(data.groups.map(group => [group.bucket, group])); + + return ( +
+

+ {t('intelligence.agentWork.subtitle')} +

+ + {BUCKET_ORDER.map(bucket => { + const group = groupByBucket.get(bucket); + const rows = group?.rows ?? []; + if (rows.length === 0) return null; + return ( +
+
+ + {bucket === 'working' && ( + + )} + {t(BUCKET_LABEL_KEY[bucket])} + + + {group?.count ?? rows.length} + +
+ +
    + {rows.map(row => ( + + ))} +
+
+ ); + })} +
+ ); +} + +interface AgentWorkRowItemProps { + row: AgentWorkRow; + onOpenThread: (threadId: string) => void; +} + +function AgentWorkRowItem({ row, onOpenThread }: AgentWorkRowItemProps) { + const { t } = useT(); + const name = row.displayName || row.agentId || row.runId; + const totalTokens = row.inputTokens + row.outputTokens; + // Localize the backend enum values, falling back to the raw value for any + // status/kind the UI doesn't yet have a key for. + const statusLabel = STATUS_LABEL_KEY[row.status] ? t(STATUS_LABEL_KEY[row.status]) : row.status; + const kindLabel = KIND_LABEL_KEY[row.kind] ? t(KIND_LABEL_KEY[row.kind]) : row.kind; + + return ( +
  • +
    +
    +
    + + {name} + + + {statusLabel} + + + {kindLabel} + +
    + {row.summary && ( +

    + {row.summary} +

    + )} + {row.error && ( +

    + {row.error} +

    + )} +
    + +
    + + {formatElapsed(row.elapsedMs)} + + {formatCost(row.costUsd)} + {formatTokens(totalTokens)} +
    +
    + + {(row.parentThreadId || row.workerThreadId) && ( +
    + {row.parentThreadId && ( + + )} + {row.workerThreadId && ( + + )} +
    + )} +
  • + ); +} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 8caa011d4..0b2d18f23 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -306,6 +306,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'أنشئ المهام وتتبعها — مهامك الخاصة بالإضافة إلى اللوحات التي يبنيها وكلاؤك عبر المحادثات.', 'memory.tab.subconscious': 'اللاوعي', + 'memory.tab.agentWork': 'عمل الوكيل', + 'memory.tab.agentWorkDescription': + 'مركز قيادة لكل تشغيل وكيل في الخلفية — مُجمّع حسب ما يحتاج إلى مُدخلاتك، وما يعمل الآن، وما اكتمل.', 'memory.tab.agents': 'المكتبة', 'memory.tab.agentsDescription': 'تصفّح وشغّل الوكلاء المتاحين — لكل منهم أدواته وقدراته ومجال تركيزه.', @@ -2766,6 +2769,35 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- الرابط: {url}', 'intelligence.workTask.closingInstruction': 'ابدأ بإعادة صياغة خطة التنفيذ الملموسة بإيجاز، ثم نفّذها. أبقِ التقدّم مرئيًا في هذا المحادثة وحدّث لوحة المهام عند تغيّر حالة العمل.', + 'intelligence.agentWork.subtitle': 'كل تشغيل وكيل في الخلفية، مُجمّع حسب حالة دورة الحياة.', + 'intelligence.agentWork.loading': 'جارٍ تحميل عمل الوكيل…', + 'intelligence.agentWork.failedToLoad': 'تعذّر تحميل عمل الوكيل', + 'intelligence.agentWork.empty': 'لا توجد عمليات تشغيل وكيل في الخلفية بعد.', + 'intelligence.agentWork.bucket.needsInput': 'يحتاج إلى مُدخلات', + 'intelligence.agentWork.bucket.working': 'قيد العمل', + 'intelligence.agentWork.bucket.completed': 'مكتمل', + 'intelligence.agentWork.bucket.failed': 'فشل', + 'intelligence.agentWork.bucket.stopped': 'متوقف', + 'intelligence.agentWork.column.agent': 'الوكيل', + 'intelligence.agentWork.column.status': 'الحالة', + 'intelligence.agentWork.column.elapsed': 'الوقت المنقضي', + 'intelligence.agentWork.column.cost': 'التكلفة', + 'intelligence.agentWork.column.tokens': 'الرموز', + 'intelligence.agentWork.status.pending': 'قيد الانتظار', + 'intelligence.agentWork.status.running': 'قيد العمل', + 'intelligence.agentWork.status.awaitingUser': 'يحتاج إلى مُدخلات', + 'intelligence.agentWork.status.paused': 'متوقف مؤقتًا', + 'intelligence.agentWork.status.completed': 'مكتمل', + 'intelligence.agentWork.status.failed': 'فشل', + 'intelligence.agentWork.status.cancelled': 'ملغى', + 'intelligence.agentWork.status.interrupted': 'مقاطَع', + 'intelligence.agentWork.kind.subagent': 'وكيل فرعي', + 'intelligence.agentWork.kind.workerThread': 'محادثة عامل', + 'intelligence.agentWork.kind.backgroundAgent': 'وكيل في الخلفية', + 'intelligence.agentWork.kind.teamMember': 'عضو الفريق', + 'intelligence.agentWork.kind.workflowChild': 'عنصر فرعي لسير العمل', + 'intelligence.agentWork.openThread': 'فتح المحادثة', + 'intelligence.agentWork.openWorker': 'فتح العامل', 'intelligence.refine.objectiveDefault': 'حوّل المهمة المصدر إلى مهمة وكيل جاهزة للتنفيذ: {title}', 'intelligence.refine.sourceLine': 'المصدر: {url}', 'intelligence.refine.sourceIntake': 'المصدر: استقبال مصدر المهام', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index cfd05a884..6abe9a022 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -310,6 +310,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'টাস্ক তৈরি করুন এবং ট্র্যাক করুন — আপনার নিজের কাজের তালিকা এবং এজেন্টরা কথোপকথন জুড়ে যে বোর্ডগুলি তৈরি করে।', 'memory.tab.subconscious': 'সাবকনশাস', + 'memory.tab.agentWork': 'এজেন্ট কাজ', + 'memory.tab.agentWorkDescription': + 'প্রতিটি ব্যাকগ্রাউন্ড এজেন্ট রানের জন্য একটি কমান্ড সেন্টার — আপনার ইনপুট প্রয়োজন এমন, চলমান এবং সম্পন্ন অনুযায়ী গোষ্ঠীবদ্ধ।', 'memory.tab.agents': 'লাইব্রেরি', 'memory.tab.agentsDescription': 'আপনার উপলব্ধ এজেন্টগুলি ব্রাউজ করুন এবং চালান — প্রতিটির নিজস্ব টুল, ক্ষমতা এবং ফোকাস এলাকা রয়েছে।', @@ -2822,6 +2825,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- ইউআরএল: {url}', 'intelligence.workTask.closingInstruction': 'প্রথমে সুনির্দিষ্ট বাস্তবায়ন পরিকল্পনাটি সংক্ষেপে পুনরায় বলুন, তারপর তা সম্পাদন করুন। এই থ্রেডে অগ্রগতি দৃশ্যমান রাখুন এবং কাজের অবস্থা পরিবর্তিত হলে টাস্ক বোর্ড আপডেট করুন।', + 'intelligence.agentWork.subtitle': + 'প্রতিটি ব্যাকগ্রাউন্ড এজেন্ট রান, জীবনচক্রের অবস্থা অনুযায়ী গোষ্ঠীবদ্ধ।', + 'intelligence.agentWork.loading': 'এজেন্ট কাজ লোড হচ্ছে…', + 'intelligence.agentWork.failedToLoad': 'এজেন্ট কাজ লোড করা যায়নি', + 'intelligence.agentWork.empty': 'এখনও কোনো ব্যাকগ্রাউন্ড এজেন্ট রান নেই।', + 'intelligence.agentWork.bucket.needsInput': 'ইনপুট প্রয়োজন', + 'intelligence.agentWork.bucket.working': 'কাজ চলছে', + 'intelligence.agentWork.bucket.completed': 'সম্পন্ন', + 'intelligence.agentWork.bucket.failed': 'ব্যর্থ', + 'intelligence.agentWork.bucket.stopped': 'বন্ধ', + 'intelligence.agentWork.column.agent': 'এজেন্ট', + 'intelligence.agentWork.column.status': 'স্ট্যাটাস', + 'intelligence.agentWork.column.elapsed': 'অতিবাহিত সময়', + 'intelligence.agentWork.column.cost': 'ব্যয়', + 'intelligence.agentWork.column.tokens': 'টোকেন', + 'intelligence.agentWork.status.pending': 'মুলতুবি', + 'intelligence.agentWork.status.running': 'চলছে', + 'intelligence.agentWork.status.awaitingUser': 'ইনপুট প্রয়োজন', + 'intelligence.agentWork.status.paused': 'বিরতিতে', + 'intelligence.agentWork.status.completed': 'সম্পন্ন', + 'intelligence.agentWork.status.failed': 'ব্যর্থ', + 'intelligence.agentWork.status.cancelled': 'বাতিল', + 'intelligence.agentWork.status.interrupted': 'বিঘ্নিত', + 'intelligence.agentWork.kind.subagent': 'সাব-এজেন্ট', + 'intelligence.agentWork.kind.workerThread': 'ওয়ার্কার থ্রেড', + 'intelligence.agentWork.kind.backgroundAgent': 'ব্যাকগ্রাউন্ড এজেন্ট', + 'intelligence.agentWork.kind.teamMember': 'টিম সদস্য', + 'intelligence.agentWork.kind.workflowChild': 'ওয়ার্কফ্লো চাইল্ড', + 'intelligence.agentWork.openThread': 'থ্রেড খুলুন', + 'intelligence.agentWork.openWorker': 'ওয়ার্কার খুলুন', 'intelligence.refine.objectiveDefault': 'উৎস টাস্কটিকে বাস্তবায়নের জন্য প্রস্তুত একটি এজেন্ট টাস্কে রূপান্তর করুন: {title}', 'intelligence.refine.sourceLine': 'উৎস: {url}', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 292fcbd84..c2c53944d 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -319,6 +319,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Aufgaben erstellen und verfolgen – eigene To-dos sowie die Boards, die Agenten in Gesprächen anlegen.', 'memory.tab.subconscious': 'Unterbewusstsein', + 'memory.tab.agentWork': 'Agentenarbeit', + 'memory.tab.agentWorkDescription': + 'Eine Kommandozentrale für jeden Hintergrund-Agentenlauf — gruppiert danach, was deine Eingabe braucht, was läuft und was abgeschlossen ist.', 'memory.tab.agents': 'Bibliothek', 'memory.tab.agentsDescription': 'Verfügbare Agenten durchsuchen und starten — jeder mit eigenen Werkzeugen, Fähigkeiten und Schwerpunkten.', @@ -2892,6 +2895,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Adresse: {url}', 'intelligence.workTask.closingInstruction': 'Beginne damit, den konkreten Umsetzungsplan kurz zu wiederholen, und führe ihn dann aus. Halte den Fortschritt in diesem Thread sichtbar und aktualisiere das Aufgabenboard, wenn sich der Arbeitsstand ändert.', + 'intelligence.agentWork.subtitle': + 'Jeder Hintergrund-Agentenlauf, gruppiert nach Lebenszyklusstatus.', + 'intelligence.agentWork.loading': 'Agentenarbeit wird geladen…', + 'intelligence.agentWork.failedToLoad': 'Agentenarbeit konnte nicht geladen werden', + 'intelligence.agentWork.empty': 'Noch keine Hintergrund-Agentenläufe.', + 'intelligence.agentWork.bucket.needsInput': 'Eingabe nötig', + 'intelligence.agentWork.bucket.working': 'In Arbeit', + 'intelligence.agentWork.bucket.completed': 'Abgeschlossen', + 'intelligence.agentWork.bucket.failed': 'Fehlgeschlagen', + 'intelligence.agentWork.bucket.stopped': 'Angehalten', + 'intelligence.agentWork.column.agent': 'Agent', + 'intelligence.agentWork.column.status': 'Status', + 'intelligence.agentWork.column.elapsed': 'Verstrichen', + 'intelligence.agentWork.column.cost': 'Kosten', + 'intelligence.agentWork.column.tokens': 'Tokens', + 'intelligence.agentWork.status.pending': 'Ausstehend', + 'intelligence.agentWork.status.running': 'Läuft', + 'intelligence.agentWork.status.awaitingUser': 'Eingabe nötig', + 'intelligence.agentWork.status.paused': 'Pausiert', + 'intelligence.agentWork.status.completed': 'Abgeschlossen', + 'intelligence.agentWork.status.failed': 'Fehlgeschlagen', + 'intelligence.agentWork.status.cancelled': 'Abgebrochen', + 'intelligence.agentWork.status.interrupted': 'Unterbrochen', + 'intelligence.agentWork.kind.subagent': 'Subagent', + 'intelligence.agentWork.kind.workerThread': 'Worker-Thread', + 'intelligence.agentWork.kind.backgroundAgent': 'Hintergrundagent', + 'intelligence.agentWork.kind.teamMember': 'Teammitglied', + 'intelligence.agentWork.kind.workflowChild': 'Workflow-Unterelement', + 'intelligence.agentWork.openThread': 'Thread öffnen', + 'intelligence.agentWork.openWorker': 'Worker öffnen', 'intelligence.refine.objectiveDefault': 'Verwandle die Quellaufgabe in eine umsetzungsbereite Agentenaufgabe: {title}', 'intelligence.refine.sourceLine': 'Quelle: {url}', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 6b5da9a20..05f6c18fc 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -335,6 +335,9 @@ const en: TranslationMap = { 'memory.tab.tasksDescription': 'Create and track tasks — your own to-dos plus the boards your agents build across conversations.', 'memory.tab.subconscious': 'Subconscious', + 'memory.tab.agentWork': 'Agent Work', + 'memory.tab.agentWorkDescription': + 'A command center for every background agent run — grouped by what needs your input, what is working, and what has finished.', 'memory.tab.agents': 'Library', 'memory.tab.agentsDescription': 'Browse and run your available agents — each with its own tools, capabilities, and focus area.', @@ -3339,6 +3342,35 @@ const en: TranslationMap = { 'intelligence.workTask.urlLine': '- URL: {url}', 'intelligence.workTask.closingInstruction': 'Start by restating the concrete implementation plan briefly, then execute it. Keep progress visible in this thread and update the task board when the work state changes.', + 'intelligence.agentWork.subtitle': 'Every background agent run, grouped by lifecycle state.', + 'intelligence.agentWork.loading': 'Loading agent work…', + 'intelligence.agentWork.failedToLoad': 'Failed to load agent work', + 'intelligence.agentWork.empty': 'No background agent runs yet.', + 'intelligence.agentWork.bucket.needsInput': 'Needs input', + 'intelligence.agentWork.bucket.working': 'Working', + 'intelligence.agentWork.bucket.completed': 'Completed', + 'intelligence.agentWork.bucket.failed': 'Failed', + 'intelligence.agentWork.bucket.stopped': 'Stopped', + 'intelligence.agentWork.column.agent': 'Agent', + 'intelligence.agentWork.column.status': 'Status', + 'intelligence.agentWork.column.elapsed': 'Elapsed', + 'intelligence.agentWork.column.cost': 'Cost', + 'intelligence.agentWork.column.tokens': 'Tokens', + 'intelligence.agentWork.status.pending': 'Pending', + 'intelligence.agentWork.status.running': 'Running', + 'intelligence.agentWork.status.awaitingUser': 'Awaiting input', + 'intelligence.agentWork.status.paused': 'Paused', + 'intelligence.agentWork.status.completed': 'Completed', + 'intelligence.agentWork.status.failed': 'Failed', + 'intelligence.agentWork.status.cancelled': 'Cancelled', + 'intelligence.agentWork.status.interrupted': 'Interrupted', + 'intelligence.agentWork.kind.subagent': 'Subagent', + 'intelligence.agentWork.kind.workerThread': 'Worker thread', + 'intelligence.agentWork.kind.backgroundAgent': 'Background agent', + 'intelligence.agentWork.kind.teamMember': 'Team member', + 'intelligence.agentWork.kind.workflowChild': 'Workflow child', + 'intelligence.agentWork.openThread': 'Open thread', + 'intelligence.agentWork.openWorker': 'Open worker', 'intelligence.refine.objectiveDefault': 'Turn the source task into an implementation-ready agent task: {title}', 'intelligence.refine.sourceLine': 'Source: {url}', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index ec115caa1..a4f8ffc26 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -319,6 +319,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Crea y realiza un seguimiento de tareas: tus propios pendientes más los tableros que tus agentes crean a lo largo de las conversaciones.', 'memory.tab.subconscious': 'Subconsciente', + 'memory.tab.agentWork': 'Trabajo del agente', + 'memory.tab.agentWorkDescription': + 'Un centro de mando para cada ejecución de agente en segundo plano, agrupado según lo que necesita tu intervención, lo que está en curso y lo que ha terminado.', 'memory.tab.agents': 'Biblioteca', 'memory.tab.agentsDescription': 'Explora y ejecuta los agentes disponibles — cada uno con sus propias herramientas, capacidades y área de enfoque.', @@ -2876,6 +2879,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Dirección: {url}', 'intelligence.workTask.closingInstruction': 'Empieza reformulando brevemente el plan de implementación concreto y luego ejecútalo. Mantén el progreso visible en este hilo y actualiza el tablero de tareas cuando cambie el estado del trabajo.', + 'intelligence.agentWork.subtitle': + 'Cada ejecución de agente en segundo plano, agrupada por estado del ciclo de vida.', + 'intelligence.agentWork.loading': 'Cargando trabajo del agente…', + 'intelligence.agentWork.failedToLoad': 'No se pudo cargar el trabajo del agente', + 'intelligence.agentWork.empty': 'Aún no hay ejecuciones de agente en segundo plano.', + 'intelligence.agentWork.bucket.needsInput': 'Requiere intervención', + 'intelligence.agentWork.bucket.working': 'En curso', + 'intelligence.agentWork.bucket.completed': 'Completado', + 'intelligence.agentWork.bucket.failed': 'Fallido', + 'intelligence.agentWork.bucket.stopped': 'Detenido', + 'intelligence.agentWork.column.agent': 'Agente', + 'intelligence.agentWork.column.status': 'Estado', + 'intelligence.agentWork.column.elapsed': 'Transcurrido', + 'intelligence.agentWork.column.cost': 'Costo', + 'intelligence.agentWork.column.tokens': 'Fichas', + 'intelligence.agentWork.status.pending': 'Pendiente', + 'intelligence.agentWork.status.running': 'En ejecución', + 'intelligence.agentWork.status.awaitingUser': 'Requiere intervención', + 'intelligence.agentWork.status.paused': 'Pausado', + 'intelligence.agentWork.status.completed': 'Completado', + 'intelligence.agentWork.status.failed': 'Fallido', + 'intelligence.agentWork.status.cancelled': 'Cancelado', + 'intelligence.agentWork.status.interrupted': 'Interrumpido', + 'intelligence.agentWork.kind.subagent': 'Subagente', + 'intelligence.agentWork.kind.workerThread': 'Hilo de worker', + 'intelligence.agentWork.kind.backgroundAgent': 'Agente en segundo plano', + 'intelligence.agentWork.kind.teamMember': 'Miembro del equipo', + 'intelligence.agentWork.kind.workflowChild': 'Hijo de flujo de trabajo', + 'intelligence.agentWork.openThread': 'Abrir hilo', + 'intelligence.agentWork.openWorker': 'Abrir trabajador', 'intelligence.refine.objectiveDefault': 'Convierte la tarea de origen en una tarea de agente lista para implementar: {title}', 'intelligence.refine.sourceLine': 'Origen: {url}', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 13da3952a..292b077e9 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -318,6 +318,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Créez et suivez des tâches — vos propres listes de choses à faire ainsi que les tableaux que vos agents construisent au fil des conversations.', 'memory.tab.subconscious': 'Subconscient', + 'memory.tab.agentWork': "Travail de l'agent", + 'memory.tab.agentWorkDescription': + "Un centre de commande pour chaque exécution d'agent en arrière-plan, regroupée selon ce qui nécessite votre intervention, ce qui est en cours et ce qui est terminé.", 'memory.tab.agents': 'Bibliothèque', 'memory.tab.agentsDescription': 'Parcourez et exécutez vos agents disponibles — chacun avec ses propres outils, capacités et domaine de spécialisation.', @@ -2890,6 +2893,36 @@ const messages: TranslationMap = { 'intelligence.refine.objectiveDefault': "Transformez la tâche source en une tâche d'agent prête à être implémentée : {title}", 'intelligence.refine.sourceLine': 'Source : {url}', + 'intelligence.agentWork.subtitle': + "Chaque exécution d'agent en arrière-plan, regroupée par état du cycle de vie.", + 'intelligence.agentWork.loading': "Chargement du travail de l'agent…", + 'intelligence.agentWork.failedToLoad': "Impossible de charger le travail de l'agent", + 'intelligence.agentWork.empty': "Aucune exécution d'agent en arrière-plan pour l'instant.", + 'intelligence.agentWork.bucket.needsInput': 'Intervention requise', + 'intelligence.agentWork.bucket.working': 'En cours', + 'intelligence.agentWork.bucket.completed': 'Terminé', + 'intelligence.agentWork.bucket.failed': 'Échoué', + 'intelligence.agentWork.bucket.stopped': 'Arrêté', + 'intelligence.agentWork.column.agent': 'Agent', + 'intelligence.agentWork.column.status': 'Statut', + 'intelligence.agentWork.column.elapsed': 'Écoulé', + 'intelligence.agentWork.column.cost': 'Coût', + 'intelligence.agentWork.column.tokens': 'Jetons', + 'intelligence.agentWork.status.pending': 'En attente', + 'intelligence.agentWork.status.running': 'En cours', + 'intelligence.agentWork.status.awaitingUser': 'Intervention requise', + 'intelligence.agentWork.status.paused': 'En pause', + 'intelligence.agentWork.status.completed': 'Terminé', + 'intelligence.agentWork.status.failed': 'Échoué', + 'intelligence.agentWork.status.cancelled': 'Annulé', + 'intelligence.agentWork.status.interrupted': 'Interrompu', + 'intelligence.agentWork.kind.subagent': 'Sous-agent', + 'intelligence.agentWork.kind.workerThread': 'Fil worker', + 'intelligence.agentWork.kind.backgroundAgent': 'Agent en arrière-plan', + 'intelligence.agentWork.kind.teamMember': 'Membre de l’équipe', + 'intelligence.agentWork.kind.workflowChild': 'Élément enfant de workflow', + 'intelligence.agentWork.openThread': 'Ouvrir le fil', + 'intelligence.agentWork.openWorker': 'Ouvrir le worker', 'intelligence.refine.sourceIntake': 'Source : réception des sources de tâches', 'intelligence.refine.repositoryLine': 'Dépôt : {repo}', 'intelligence.refine.externalTaskLine': 'Tâche externe : {externalId}', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 3d0e69051..7832cb5c5 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -309,6 +309,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'कार्य बनाएं और ट्रैक करें — आपके अपने टू-डू और साथ ही वे बोर्ड जो आपके एजेंट वार्तालापों में बनाते हैं।', 'memory.tab.subconscious': 'सबकॉन्शस', + 'memory.tab.agentWork': 'एजेंट कार्य', + 'memory.tab.agentWorkDescription': + 'हर पृष्ठभूमि एजेंट रन के लिए एक कमांड सेंटर — इस आधार पर समूहित कि किसे आपकी जानकारी चाहिए, क्या चल रहा है और क्या पूरा हो चुका है।', 'memory.tab.agents': 'लाइब्रेरी', 'memory.tab.agentsDescription': 'अपने उपलब्ध एजेंट ब्राउज़ करें और चलाएं — प्रत्येक के पास अपने टूल, क्षमताएं और फोकस क्षेत्र हैं।', @@ -2827,6 +2830,35 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- यूआरएल: {url}', 'intelligence.workTask.closingInstruction': 'पहले ठोस कार्यान्वयन योजना को संक्षेप में दोहराएँ, फिर उसे निष्पादित करें। इस थ्रेड में प्रगति दिखती रहे और कार्य की स्थिति बदलने पर कार्य बोर्ड अपडेट करें।', + 'intelligence.agentWork.subtitle': 'हर पृष्ठभूमि एजेंट रन, जीवनचक्र स्थिति के अनुसार समूहित।', + 'intelligence.agentWork.loading': 'एजेंट कार्य लोड हो रहा है…', + 'intelligence.agentWork.failedToLoad': 'एजेंट कार्य लोड नहीं हो सका', + 'intelligence.agentWork.empty': 'अभी तक कोई पृष्ठभूमि एजेंट रन नहीं।', + 'intelligence.agentWork.bucket.needsInput': 'इनपुट चाहिए', + 'intelligence.agentWork.bucket.working': 'काम जारी', + 'intelligence.agentWork.bucket.completed': 'पूरा हुआ', + 'intelligence.agentWork.bucket.failed': 'विफल', + 'intelligence.agentWork.bucket.stopped': 'रुका हुआ', + 'intelligence.agentWork.column.agent': 'एजेंट', + 'intelligence.agentWork.column.status': 'स्टेटस', + 'intelligence.agentWork.column.elapsed': 'बीता समय', + 'intelligence.agentWork.column.cost': 'लागत', + 'intelligence.agentWork.column.tokens': 'टोकन', + 'intelligence.agentWork.status.pending': 'लंबित', + 'intelligence.agentWork.status.running': 'चल रहा है', + 'intelligence.agentWork.status.awaitingUser': 'इनपुट चाहिए', + 'intelligence.agentWork.status.paused': 'रुका हुआ', + 'intelligence.agentWork.status.completed': 'पूरा हुआ', + 'intelligence.agentWork.status.failed': 'विफल', + 'intelligence.agentWork.status.cancelled': 'रद्द हुआ', + 'intelligence.agentWork.status.interrupted': 'बाधित', + 'intelligence.agentWork.kind.subagent': 'सब-एजेंट', + 'intelligence.agentWork.kind.workerThread': 'वर्कर थ्रेड', + 'intelligence.agentWork.kind.backgroundAgent': 'बैकग्राउंड एजेंट', + 'intelligence.agentWork.kind.teamMember': 'टीम सदस्य', + 'intelligence.agentWork.kind.workflowChild': 'वर्कफ़्लो चाइल्ड', + 'intelligence.agentWork.openThread': 'थ्रेड खोलें', + 'intelligence.agentWork.openWorker': 'वर्कर खोलें', 'intelligence.refine.objectiveDefault': 'स्रोत कार्य को कार्यान्वयन के लिए तैयार एजेंट कार्य में बदलें: {title}', 'intelligence.refine.sourceLine': 'स्रोत: {url}', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 78e260519..73978f481 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -311,6 +311,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Buat dan lacak tugas — to-do Anda sendiri beserta papan yang dibangun agen Anda di berbagai percakapan.', 'memory.tab.subconscious': 'Bawah sadar', + 'memory.tab.agentWork': 'Kerja agen', + 'memory.tab.agentWorkDescription': + 'Pusat kendali untuk setiap proses agen latar belakang — dikelompokkan berdasarkan apa yang butuh masukan Anda, apa yang sedang berjalan, dan apa yang sudah selesai.', 'memory.tab.agents': 'Perpustakaan', 'memory.tab.agentsDescription': 'Telusuri dan jalankan agen yang tersedia — masing-masing dengan alat, kemampuan, dan area fokusnya sendiri.', @@ -2829,6 +2832,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Tautan: {url}', 'intelligence.workTask.closingInstruction': 'Mulailah dengan menyatakan kembali rencana implementasi konkret secara singkat, lalu jalankan. Jaga agar kemajuan tetap terlihat di utas ini dan perbarui papan tugas ketika status pekerjaan berubah.', + 'intelligence.agentWork.subtitle': + 'Setiap proses agen latar belakang, dikelompokkan menurut status siklus hidup.', + 'intelligence.agentWork.loading': 'Memuat kerja agen…', + 'intelligence.agentWork.failedToLoad': 'Gagal memuat kerja agen', + 'intelligence.agentWork.empty': 'Belum ada proses agen latar belakang.', + 'intelligence.agentWork.bucket.needsInput': 'Perlu masukan', + 'intelligence.agentWork.bucket.working': 'Berjalan', + 'intelligence.agentWork.bucket.completed': 'Selesai', + 'intelligence.agentWork.bucket.failed': 'Gagal', + 'intelligence.agentWork.bucket.stopped': 'Berhenti', + 'intelligence.agentWork.column.agent': 'Agen', + 'intelligence.agentWork.column.status': 'Status', + 'intelligence.agentWork.column.elapsed': 'Berlalu', + 'intelligence.agentWork.column.cost': 'Biaya', + 'intelligence.agentWork.column.tokens': 'Token', + 'intelligence.agentWork.status.pending': 'Tertunda', + 'intelligence.agentWork.status.running': 'Berjalan', + 'intelligence.agentWork.status.awaitingUser': 'Perlu masukan', + 'intelligence.agentWork.status.paused': 'Dijeda', + 'intelligence.agentWork.status.completed': 'Selesai', + 'intelligence.agentWork.status.failed': 'Gagal', + 'intelligence.agentWork.status.cancelled': 'Dibatalkan', + 'intelligence.agentWork.status.interrupted': 'Terputus', + 'intelligence.agentWork.kind.subagent': 'Subagen', + 'intelligence.agentWork.kind.workerThread': 'Thread worker', + 'intelligence.agentWork.kind.backgroundAgent': 'Agen latar belakang', + 'intelligence.agentWork.kind.teamMember': 'Anggota tim', + 'intelligence.agentWork.kind.workflowChild': 'Anak alur kerja', + 'intelligence.agentWork.openThread': 'Buka utas', + 'intelligence.agentWork.openWorker': 'Buka pekerja', 'intelligence.refine.objectiveDefault': 'Ubah tugas sumber menjadi tugas agen yang siap diimplementasikan: {title}', 'intelligence.refine.sourceLine': 'Sumber: {url}', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 9d6ce1dd2..c2cc4d9f1 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -316,6 +316,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Crea e monitora le attività — i tuoi to-do personali e le board create dagli agenti nelle conversazioni.', 'memory.tab.subconscious': 'Subconscio', + 'memory.tab.agentWork': "Lavoro dell'agente", + 'memory.tab.agentWorkDescription': + 'Un centro di comando per ogni esecuzione di agente in background, raggruppata in base a ciò che richiede il tuo intervento, ciò che è in corso e ciò che è terminato.', 'memory.tab.agents': 'Libreria', 'memory.tab.agentsDescription': 'Sfoglia e avvia gli agenti disponibili — ognuno con i propri strumenti, capacità e area di specializzazione.', @@ -2870,6 +2873,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Indirizzo: {url}', 'intelligence.workTask.closingInstruction': 'Inizia riformulando brevemente il piano di implementazione concreto, poi eseguilo. Mantieni i progressi visibili in questo thread e aggiorna la bacheca delle attività quando cambia lo stato del lavoro.', + 'intelligence.agentWork.subtitle': + 'Ogni esecuzione di agente in background, raggruppata per stato del ciclo di vita.', + 'intelligence.agentWork.loading': "Caricamento del lavoro dell'agente…", + 'intelligence.agentWork.failedToLoad': "Impossibile caricare il lavoro dell'agente", + 'intelligence.agentWork.empty': 'Ancora nessuna esecuzione di agente in background.', + 'intelligence.agentWork.bucket.needsInput': 'Richiede intervento', + 'intelligence.agentWork.bucket.working': 'In corso', + 'intelligence.agentWork.bucket.completed': 'Completato', + 'intelligence.agentWork.bucket.failed': 'Fallito', + 'intelligence.agentWork.bucket.stopped': 'Fermato', + 'intelligence.agentWork.column.agent': 'Agente', + 'intelligence.agentWork.column.status': 'Stato', + 'intelligence.agentWork.column.elapsed': 'Trascorso', + 'intelligence.agentWork.column.cost': 'Costo', + 'intelligence.agentWork.column.tokens': 'Gettoni', + 'intelligence.agentWork.status.pending': 'In sospeso', + 'intelligence.agentWork.status.running': 'In esecuzione', + 'intelligence.agentWork.status.awaitingUser': 'Richiede intervento', + 'intelligence.agentWork.status.paused': 'In pausa', + 'intelligence.agentWork.status.completed': 'Completato', + 'intelligence.agentWork.status.failed': 'Fallito', + 'intelligence.agentWork.status.cancelled': 'Annullato', + 'intelligence.agentWork.status.interrupted': 'Interrotto', + 'intelligence.agentWork.kind.subagent': 'Sottoagente', + 'intelligence.agentWork.kind.workerThread': 'Thread worker', + 'intelligence.agentWork.kind.backgroundAgent': 'Agente in background', + 'intelligence.agentWork.kind.teamMember': 'Membro del team', + 'intelligence.agentWork.kind.workflowChild': 'Figlio del flusso di lavoro', + 'intelligence.agentWork.openThread': 'Apri thread', + 'intelligence.agentWork.openWorker': 'Apri worker', 'intelligence.refine.objectiveDefault': "Trasforma l'attività di origine in un'attività dell'agente pronta per l'implementazione: {title}", 'intelligence.refine.sourceLine': 'Origine: {url}', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index a8858680b..954a28865 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -309,6 +309,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': '작업을 만들고 추적하세요 — 본인의 할 일 목록과 에이전트가 대화를 통해 구성한 보드가 모두 포함됩니다.', 'memory.tab.subconscious': '잠재의식', + 'memory.tab.agentWork': '에이전트 작업', + 'memory.tab.agentWorkDescription': + '모든 백그라운드 에이전트 실행을 위한 명령 센터 — 입력이 필요한 것, 진행 중인 것, 완료된 것별로 그룹화됩니다.', 'memory.tab.agents': '라이브러리', 'memory.tab.agentsDescription': '사용 가능한 에이전트를 탐색하고 실행하세요 — 각 에이전트는 고유한 도구, 기능 및 전문 분야를 갖추고 있습니다.', @@ -2800,6 +2803,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- 링크: {url}', 'intelligence.workTask.closingInstruction': '먼저 구체적인 구현 계획을 간략히 다시 설명한 다음 실행하세요. 이 스레드에서 진행 상황을 계속 보이게 하고 작업 상태가 바뀌면 작업 보드를 업데이트하세요.', + 'intelligence.agentWork.subtitle': + '모든 백그라운드 에이전트 실행을 수명 주기 상태별로 그룹화합니다.', + 'intelligence.agentWork.loading': '에이전트 작업 불러오는 중…', + 'intelligence.agentWork.failedToLoad': '에이전트 작업을 불러오지 못했습니다', + 'intelligence.agentWork.empty': '아직 백그라운드 에이전트 실행이 없습니다.', + 'intelligence.agentWork.bucket.needsInput': '입력 필요', + 'intelligence.agentWork.bucket.working': '작업 중', + 'intelligence.agentWork.bucket.completed': '완료됨', + 'intelligence.agentWork.bucket.failed': '실패', + 'intelligence.agentWork.bucket.stopped': '중지됨', + 'intelligence.agentWork.column.agent': '에이전트', + 'intelligence.agentWork.column.status': '상태', + 'intelligence.agentWork.column.elapsed': '경과 시간', + 'intelligence.agentWork.column.cost': '비용', + 'intelligence.agentWork.column.tokens': '토큰', + 'intelligence.agentWork.status.pending': '대기 중', + 'intelligence.agentWork.status.running': '실행 중', + 'intelligence.agentWork.status.awaitingUser': '입력 필요', + 'intelligence.agentWork.status.paused': '일시 중지됨', + 'intelligence.agentWork.status.completed': '완료됨', + 'intelligence.agentWork.status.failed': '실패', + 'intelligence.agentWork.status.cancelled': '취소됨', + 'intelligence.agentWork.status.interrupted': '중단됨', + 'intelligence.agentWork.kind.subagent': '하위 에이전트', + 'intelligence.agentWork.kind.workerThread': '워커 스레드', + 'intelligence.agentWork.kind.backgroundAgent': '백그라운드 에이전트', + 'intelligence.agentWork.kind.teamMember': '팀 구성원', + 'intelligence.agentWork.kind.workflowChild': '워크플로 하위 항목', + 'intelligence.agentWork.openThread': '스레드 열기', + 'intelligence.agentWork.openWorker': '워커 열기', 'intelligence.refine.objectiveDefault': '소스 작업을 구현 준비가 된 에이전트 작업으로 전환하세요: {title}', 'intelligence.refine.sourceLine': '소스: {url}', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 95569c61d..dd4920a34 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -314,6 +314,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Twórz i śledź zadania — własne listy do zrobienia oraz tablice, które Twoi agenci budują w rozmowach.', 'memory.tab.subconscious': 'Podświadomość', + 'memory.tab.agentWork': 'Praca agenta', + 'memory.tab.agentWorkDescription': + 'Centrum dowodzenia dla każdego przebiegu agenta w tle — pogrupowane według tego, co wymaga Twojej reakcji, co działa i co zostało ukończone.', 'memory.tab.agents': 'Biblioteka', 'memory.tab.agentsDescription': 'Przeglądaj i uruchamiaj dostępnych agentów — każdy z własnymi narzędziami, możliwościami i obszarem specjalizacji.', @@ -2861,6 +2864,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Adres: {url}', 'intelligence.workTask.closingInstruction': 'Zacznij od krótkiego powtórzenia konkretnego planu wdrożenia, a następnie go zrealizuj. Utrzymuj widoczność postępów w tym wątku i aktualizuj tablicę zadań, gdy zmienia się stan pracy.', + 'intelligence.agentWork.subtitle': + 'Każdy przebieg agenta w tle, pogrupowany według stanu cyklu życia.', + 'intelligence.agentWork.loading': 'Ładowanie pracy agenta…', + 'intelligence.agentWork.failedToLoad': 'Nie udało się załadować pracy agenta', + 'intelligence.agentWork.empty': 'Brak przebiegów agenta w tle.', + 'intelligence.agentWork.bucket.needsInput': 'Wymaga reakcji', + 'intelligence.agentWork.bucket.working': 'W toku', + 'intelligence.agentWork.bucket.completed': 'Ukończone', + 'intelligence.agentWork.bucket.failed': 'Niepowodzenie', + 'intelligence.agentWork.bucket.stopped': 'Zatrzymane', + 'intelligence.agentWork.column.agent': 'Agent', + 'intelligence.agentWork.column.status': 'Status', + 'intelligence.agentWork.column.elapsed': 'Czas trwania', + 'intelligence.agentWork.column.cost': 'Koszt', + 'intelligence.agentWork.column.tokens': 'Tokeny', + 'intelligence.agentWork.status.pending': 'Oczekujące', + 'intelligence.agentWork.status.running': 'W toku', + 'intelligence.agentWork.status.awaitingUser': 'Wymaga reakcji', + 'intelligence.agentWork.status.paused': 'Wstrzymane', + 'intelligence.agentWork.status.completed': 'Ukończone', + 'intelligence.agentWork.status.failed': 'Niepowodzenie', + 'intelligence.agentWork.status.cancelled': 'Anulowane', + 'intelligence.agentWork.status.interrupted': 'Przerwane', + 'intelligence.agentWork.kind.subagent': 'Podagent', + 'intelligence.agentWork.kind.workerThread': 'Wątek workera', + 'intelligence.agentWork.kind.backgroundAgent': 'Agent w tle', + 'intelligence.agentWork.kind.teamMember': 'Członek zespołu', + 'intelligence.agentWork.kind.workflowChild': 'Element podrzędny przepływu', + 'intelligence.agentWork.openThread': 'Otwórz wątek', + 'intelligence.agentWork.openWorker': 'Otwórz proces', 'intelligence.refine.objectiveDefault': 'Przekształć zadanie źródłowe w gotowe do wdrożenia zadanie agenta: {title}', 'intelligence.refine.sourceLine': 'Źródło: {url}', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index d585ea69d..edf59a7b3 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -318,6 +318,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Crie e acompanhe tarefas — seus próprios afazeres e os painéis que seus agentes constroem ao longo das conversas.', 'memory.tab.subconscious': 'Subconsciente', + 'memory.tab.agentWork': 'Trabalho do agente', + 'memory.tab.agentWorkDescription': + 'Um centro de comando para cada execução de agente em segundo plano, agrupada conforme o que precisa da sua intervenção, o que está em andamento e o que foi concluído.', 'memory.tab.agents': 'Biblioteca', 'memory.tab.agentsDescription': 'Navegue e execute os agentes disponíveis — cada um com suas próprias ferramentas, capacidades e área de foco.', @@ -2873,6 +2876,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Endereço: {url}', 'intelligence.workTask.closingInstruction': 'Comece reformulando brevemente o plano de implementação concreto e depois execute-o. Mantenha o progresso visível neste tópico e atualize o quadro de tarefas quando o estado do trabalho mudar.', + 'intelligence.agentWork.subtitle': + 'Cada execução de agente em segundo plano, agrupada por estado do ciclo de vida.', + 'intelligence.agentWork.loading': 'Carregando trabalho do agente…', + 'intelligence.agentWork.failedToLoad': 'Não foi possível carregar o trabalho do agente', + 'intelligence.agentWork.empty': 'Ainda não há execuções de agente em segundo plano.', + 'intelligence.agentWork.bucket.needsInput': 'Requer intervenção', + 'intelligence.agentWork.bucket.working': 'Em andamento', + 'intelligence.agentWork.bucket.completed': 'Concluído', + 'intelligence.agentWork.bucket.failed': 'Falhou', + 'intelligence.agentWork.bucket.stopped': 'Parado', + 'intelligence.agentWork.column.agent': 'Agente', + 'intelligence.agentWork.column.status': 'Status', + 'intelligence.agentWork.column.elapsed': 'Decorrido', + 'intelligence.agentWork.column.cost': 'Custo', + 'intelligence.agentWork.column.tokens': 'Tokens', + 'intelligence.agentWork.status.pending': 'Pendente', + 'intelligence.agentWork.status.running': 'Em andamento', + 'intelligence.agentWork.status.awaitingUser': 'Requer intervenção', + 'intelligence.agentWork.status.paused': 'Pausado', + 'intelligence.agentWork.status.completed': 'Concluído', + 'intelligence.agentWork.status.failed': 'Falhou', + 'intelligence.agentWork.status.cancelled': 'Cancelado', + 'intelligence.agentWork.status.interrupted': 'Interrompido', + 'intelligence.agentWork.kind.subagent': 'Subagente', + 'intelligence.agentWork.kind.workerThread': 'Thread de worker', + 'intelligence.agentWork.kind.backgroundAgent': 'Agente em segundo plano', + 'intelligence.agentWork.kind.teamMember': 'Membro da equipe', + 'intelligence.agentWork.kind.workflowChild': 'Filho de fluxo de trabalho', + 'intelligence.agentWork.openThread': 'Abrir tópico', + 'intelligence.agentWork.openWorker': 'Abrir trabalhador', 'intelligence.refine.objectiveDefault': 'Transforme a tarefa de origem em uma tarefa de agente pronta para implementação: {title}', 'intelligence.refine.sourceLine': 'Origem: {url}', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index bb0d6acb8..a2d9aa4d2 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -311,6 +311,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': 'Создавайте и отслеживайте задачи — ваши личные дела и доски, которые агенты формируют в ходе разговоров.', 'memory.tab.subconscious': 'Подсознание', + 'memory.tab.agentWork': 'Работа агента', + 'memory.tab.agentWorkDescription': + 'Центр управления для каждого фонового запуска агента — сгруппировано по тому, что требует вашего участия, что выполняется и что завершено.', 'memory.tab.agents': 'Библиотека', 'memory.tab.agentsDescription': 'Просматривайте и запускайте доступных агентов — у каждого свои инструменты, возможности и область специализации.', @@ -2846,6 +2849,36 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- Ссылка: {url}', 'intelligence.workTask.closingInstruction': 'Начните с краткого повторения конкретного плана реализации, затем выполните его. Поддерживайте видимость прогресса в этой ветке и обновляйте доску задач при изменении состояния работы.', + 'intelligence.agentWork.subtitle': + 'Каждый фоновый запуск агента, сгруппированный по состоянию жизненного цикла.', + 'intelligence.agentWork.loading': 'Загрузка работы агента…', + 'intelligence.agentWork.failedToLoad': 'Не удалось загрузить работу агента', + 'intelligence.agentWork.empty': 'Фоновых запусков агента пока нет.', + 'intelligence.agentWork.bucket.needsInput': 'Нужны данные', + 'intelligence.agentWork.bucket.working': 'В работе', + 'intelligence.agentWork.bucket.completed': 'Завершено', + 'intelligence.agentWork.bucket.failed': 'Ошибка', + 'intelligence.agentWork.bucket.stopped': 'Остановлено', + 'intelligence.agentWork.column.agent': 'Агент', + 'intelligence.agentWork.column.status': 'Статус', + 'intelligence.agentWork.column.elapsed': 'Прошло', + 'intelligence.agentWork.column.cost': 'Расходы', + 'intelligence.agentWork.column.tokens': 'Токены', + 'intelligence.agentWork.status.pending': 'В ожидании', + 'intelligence.agentWork.status.running': 'В работе', + 'intelligence.agentWork.status.awaitingUser': 'Нужны данные', + 'intelligence.agentWork.status.paused': 'Приостановлено', + 'intelligence.agentWork.status.completed': 'Завершено', + 'intelligence.agentWork.status.failed': 'Ошибка', + 'intelligence.agentWork.status.cancelled': 'Отменено', + 'intelligence.agentWork.status.interrupted': 'Прервано', + 'intelligence.agentWork.kind.subagent': 'Субагент', + 'intelligence.agentWork.kind.workerThread': 'Чат воркера', + 'intelligence.agentWork.kind.backgroundAgent': 'Фоновый агент', + 'intelligence.agentWork.kind.teamMember': 'Участник команды', + 'intelligence.agentWork.kind.workflowChild': 'Дочерний элемент рабочего процесса', + 'intelligence.agentWork.openThread': 'Открыть тред', + 'intelligence.agentWork.openWorker': 'Открыть воркер', 'intelligence.refine.objectiveDefault': 'Превратите исходную задачу в готовую к реализации задачу агента: {title}', 'intelligence.refine.sourceLine': 'Источник: {url}', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index bd2fa67c7..4be456726 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -296,6 +296,9 @@ const messages: TranslationMap = { 'memory.tab.tasksDescription': '创建并跟踪任务——包括您自己的待办事项以及智能体在对话中创建的看板。', 'memory.tab.subconscious': '潜意识', + 'memory.tab.agentWork': '智能体工作', + 'memory.tab.agentWorkDescription': + '所有后台智能体运行的指挥中心——按需要你处理的、正在运行的和已完成的进行分组。', 'memory.tab.agents': '资料库', 'memory.tab.agentsDescription': '浏览并运行可用的智能体——每个智能体都有自己的工具、能力和专注领域。', @@ -2685,6 +2688,35 @@ const messages: TranslationMap = { 'intelligence.workTask.urlLine': '- 网址:{url}', 'intelligence.workTask.closingInstruction': '先简要重述具体的实施计划,然后执行它。在此线程中保持进度可见,并在工作状态变化时更新任务看板。', + 'intelligence.agentWork.subtitle': '所有后台智能体运行,按生命周期状态分组。', + 'intelligence.agentWork.loading': '正在加载智能体工作…', + 'intelligence.agentWork.failedToLoad': '无法加载智能体工作', + 'intelligence.agentWork.empty': '暂无后台智能体运行。', + 'intelligence.agentWork.bucket.needsInput': '需要输入', + 'intelligence.agentWork.bucket.working': '进行中', + 'intelligence.agentWork.bucket.completed': '已完成', + 'intelligence.agentWork.bucket.failed': '失败', + 'intelligence.agentWork.bucket.stopped': '已停止', + 'intelligence.agentWork.column.agent': '智能体', + 'intelligence.agentWork.column.status': '状态', + 'intelligence.agentWork.column.elapsed': '已用时间', + 'intelligence.agentWork.column.cost': '成本', + 'intelligence.agentWork.column.tokens': '令牌', + 'intelligence.agentWork.status.pending': '待定', + 'intelligence.agentWork.status.running': '进行中', + 'intelligence.agentWork.status.awaitingUser': '需要输入', + 'intelligence.agentWork.status.paused': '已暂停', + 'intelligence.agentWork.status.completed': '已完成', + 'intelligence.agentWork.status.failed': '失败', + 'intelligence.agentWork.status.cancelled': '已取消', + 'intelligence.agentWork.status.interrupted': '已中断', + 'intelligence.agentWork.kind.subagent': '子智能体', + 'intelligence.agentWork.kind.workerThread': '工作线程', + 'intelligence.agentWork.kind.backgroundAgent': '后台智能体', + 'intelligence.agentWork.kind.teamMember': '团队成员', + 'intelligence.agentWork.kind.workflowChild': '工作流子项', + 'intelligence.agentWork.openThread': '打开会话', + 'intelligence.agentWork.openWorker': '打开工作线程', 'intelligence.refine.objectiveDefault': '将来源任务转化为可立即实施的智能体任务:{title}', 'intelligence.refine.sourceLine': '来源:{url}', 'intelligence.refine.sourceIntake': '来源:任务来源接收', diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx index 0d26ad3fb..4262baf1a 100644 --- a/app/src/pages/Intelligence.tsx +++ b/app/src/pages/Intelligence.tsx @@ -3,6 +3,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 IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab'; import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab'; import MemorySection from '../components/intelligence/MemorySection'; @@ -22,12 +23,20 @@ import type { } from '../types/intelligence'; import { IS_DEV } from '../utils/config'; -type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'agents' | 'workflows' | 'council'; +type IntelligenceTab = + | 'memory' + | 'subconscious' + | 'tasks' + | 'agent-work' + | 'agents' + | 'workflows' + | 'council'; const INTELLIGENCE_TABS: IntelligenceTab[] = [ 'memory', 'subconscious', 'tasks', + 'agent-work', 'agents', 'workflows', 'council', @@ -128,6 +137,11 @@ export default function Intelligence() { devOnly?: boolean; }[] = [ { id: 'tasks', label: t('memory.tab.tasks'), description: t('memory.tab.tasksDescription') }, + { + id: 'agent-work', + label: t('memory.tab.agentWork'), + description: t('memory.tab.agentWorkDescription'), + }, { id: 'memory', label: t('memory.tab.memory') }, { id: 'subconscious', label: t('memory.tab.subconscious') }, { @@ -215,6 +229,8 @@ export default function Intelligence() { {activeTab === 'tasks' && } + {activeTab === 'agent-work' && } + {activeTab === 'agents' && } {activeTab === 'workflows' && } diff --git a/app/src/services/api/agentWorkApi.test.ts b/app/src/services/api/agentWorkApi.test.ts new file mode 100644 index 000000000..4dfb48a7a --- /dev/null +++ b/app/src/services/api/agentWorkApi.test.ts @@ -0,0 +1,88 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { callCoreRpc } from '../coreRpcClient'; +import { agentWorkApi, type AgentWorkResponse } from './agentWorkApi'; + +vi.mock('../coreRpcClient', () => ({ callCoreRpc: vi.fn() })); + +const mockCall = vi.mocked(callCoreRpc); + +function response(): AgentWorkResponse { + return { + total: 2, + groups: [ + { bucket: 'needs_input', count: 0, rows: [] }, + { + bucket: 'working', + count: 1, + rows: [ + { + runId: 'run-1', + kind: 'subagent', + agentId: 'agent-a', + displayName: 'Researcher', + bucket: 'working', + status: 'running', + workerThreadId: 'thread-w', + startedAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:01:00Z', + elapsedMs: 60000, + inputTokens: 100, + outputTokens: 50, + costUsd: 0.01, + toolCount: 2, + }, + ], + }, + { bucket: 'completed', count: 1, rows: [] }, + { bucket: 'failed', count: 0, rows: [] }, + { bucket: 'stopped', count: 0, rows: [] }, + ], + }; +} + +describe('agentWorkApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('list calls the agent_work_list RPC with no params when limit is omitted', async () => { + mockCall.mockResolvedValueOnce(response()); + await agentWorkApi.list(); + expect(mockCall).toHaveBeenCalledWith({ method: 'openhuman.agent_work_list', params: {} }); + }); + + it('list forwards an explicit limit', async () => { + mockCall.mockResolvedValueOnce(response()); + await agentWorkApi.list(25); + expect(mockCall).toHaveBeenCalledWith({ + method: 'openhuman.agent_work_list', + params: { limit: 25 }, + }); + }); + + it('list rejects a non-positive or non-integer limit without calling core', async () => { + await expect(agentWorkApi.list(0)).rejects.toThrow('positive integer'); + await expect(agentWorkApi.list(-5)).rejects.toThrow('positive integer'); + await expect(agentWorkApi.list(1.5)).rejects.toThrow('positive integer'); + expect(mockCall).not.toHaveBeenCalled(); + }); + + it('list returns the grouped response unchanged (wire is already camelCase)', async () => { + mockCall.mockResolvedValueOnce(response()); + const result = await agentWorkApi.list(); + expect(result.total).toBe(2); + expect(result.groups).toHaveLength(5); + expect(result.groups.map(g => g.bucket)).toEqual([ + 'needs_input', + 'working', + 'completed', + 'failed', + 'stopped', + ]); + const working = result.groups.find(g => g.bucket === 'working'); + expect(working?.count).toBe(1); + expect(working?.rows[0].displayName).toBe('Researcher'); + expect(working?.rows[0].workerThreadId).toBe('thread-w'); + }); +}); diff --git a/app/src/services/api/agentWorkApi.ts b/app/src/services/api/agentWorkApi.ts new file mode 100644 index 000000000..a184b5bb6 --- /dev/null +++ b/app/src/services/api/agentWorkApi.ts @@ -0,0 +1,76 @@ +/** + * Frontend client for the Background Agent Command Center surface + * (`openhuman.agent_work_list`). The Rust handler aggregates every tracked + * background agent run into five lifecycle buckets and returns the rows + * pre-grouped so the UI renders them in a stable order. + * + * The wire payload is already camelCase (the Rust controller serializes with + * `#[serde(rename_all = "camelCase")]`), so this client only types the shape + * and forwards the optional `limit`. No snake/camel transform is needed. + */ +import debug from 'debug'; + +import { callCoreRpc } from '../coreRpcClient'; + +const log = debug('agentWorkApi'); + +/** + * Lifecycle bucket a run is sorted into. The handler always emits all five, + * in this display order, even when a bucket has zero rows. + */ +export type AgentWorkBucket = 'needs_input' | 'working' | 'completed' | 'failed' | 'stopped'; + +/** A single background agent run. Mirrors the Rust `AgentWorkRow`. */ +export interface AgentWorkRow { + runId: string; + kind: string; + agentId?: string; + displayName?: string; + bucket: AgentWorkBucket; + status: string; + parentThreadId?: string; + workerThreadId?: string; + summary?: string; + error?: string; + startedAt: string; + updatedAt: string; + elapsedMs?: number; + inputTokens: number; + outputTokens: number; + costUsd: number; + toolCount: number; +} + +/** A bucket of rows plus its precomputed count. Mirrors the Rust group. */ +export interface AgentWorkGroup { + bucket: AgentWorkBucket; + count: number; + rows: AgentWorkRow[]; +} + +/** Full response from `openhuman.agent_work_list`. */ +export interface AgentWorkResponse { + groups: AgentWorkGroup[]; + total: number; +} + +export const agentWorkApi = { + /** + * List all tracked background agent runs, grouped by lifecycle bucket. + * + * @param limit Optional cap on the number of rows returned (newest first, + * applied server-side). Omit to use the handler default. + */ + list: async (limit?: number): Promise => { + if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) { + throw new Error('agentWorkApi.list: limit must be a positive integer'); + } + log('list limit=%o', limit); + const response = await callCoreRpc({ + method: 'openhuman.agent_work_list', + params: limit === undefined ? {} : { limit }, + }); + log('list received groups=%d total=%d', response.groups.length, response.total); + return response; + }, +};