mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-27 01:16:23 +02:00
feat: add background agents with scheduling support
- Add background task scheduling system with cron-based triggers - Add background-task-detail component for viewing agent status - Add agent schedule repo and state management - Update sidebar to show background agents section - Remove old workflow-authoring and workflow-run-ops skills - Add IPC handlers for agent schedule operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
82db06d724
commit
c447a42d07
20 changed files with 1544 additions and 500 deletions
|
|
@ -24,6 +24,9 @@ import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
|
|||
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
||||
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
||||
import * as composioHandler from './composio-handler.js';
|
||||
import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
|
||||
import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
|
||||
import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
|
||||
|
||||
type InvokeChannels = ipc.InvokeChannels;
|
||||
type IPCChannels = ipc.IPCChannels;
|
||||
|
|
@ -384,5 +387,38 @@ export function setupIpcHandlers() {
|
|||
'composio:execute-action': async (_event, args) => {
|
||||
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
|
||||
},
|
||||
// Agent schedule handlers
|
||||
'agent-schedule:getConfig': async () => {
|
||||
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||
try {
|
||||
return await repo.getConfig();
|
||||
} catch {
|
||||
// Return empty config if file doesn't exist
|
||||
return { agents: {} };
|
||||
}
|
||||
},
|
||||
'agent-schedule:getState': async () => {
|
||||
const repo = container.resolve<IAgentScheduleStateRepo>('agentScheduleStateRepo');
|
||||
try {
|
||||
return await repo.getState();
|
||||
} catch {
|
||||
// Return empty state if file doesn't exist
|
||||
return { agents: {} };
|
||||
}
|
||||
},
|
||||
'agent-schedule:updateAgent': async (_event, args) => {
|
||||
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||
await repo.upsert(args.agentName, args.entry);
|
||||
// Trigger the runner to pick up the change immediately
|
||||
triggerAgentScheduleRun();
|
||||
return { success: true };
|
||||
},
|
||||
'agent-schedule:deleteAgent': async (_event, args) => {
|
||||
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||
const stateRepo = container.resolve<IAgentScheduleStateRepo>('agentScheduleStateRepo');
|
||||
await repo.delete(args.agentName);
|
||||
await stateRepo.deleteAgentState(args.agentName);
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies
|
|||
import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js";
|
||||
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js";
|
||||
import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js";
|
||||
import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js";
|
||||
import { initConfigs } from "@x/core/dist/config/initConfigs.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
|
@ -156,6 +157,9 @@ app.whenReady().then(async () => {
|
|||
// start pre-built agent runner
|
||||
initPreBuiltRunner();
|
||||
|
||||
// start background agent runner (scheduled agents)
|
||||
initAgentRunner();
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ import { Separator } from "@/components/ui/separator"
|
|||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
||||
import { OnboardingModal } from '@/components/onboarding-modal'
|
||||
import { BackgroundTaskDetail } from '@/components/background-task-detail'
|
||||
import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js'
|
||||
import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js'
|
||||
|
||||
type DirEntry = z.infer<typeof workspace.DirEntry>
|
||||
type RunEventType = z.infer<typeof RunEvent>
|
||||
|
|
@ -499,6 +502,22 @@ function App() {
|
|||
// Onboarding state
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
|
||||
// Background tasks state
|
||||
type BackgroundTaskItem = {
|
||||
name: string
|
||||
description?: string
|
||||
schedule: z.infer<typeof AgentScheduleConfig>["agents"][string]["schedule"]
|
||||
enabled: boolean
|
||||
startingMessage?: string
|
||||
status?: z.infer<typeof AgentScheduleState>["agents"][string]["status"]
|
||||
nextRunAt?: string | null
|
||||
lastRunAt?: string | null
|
||||
lastError?: string | null
|
||||
runCount?: number
|
||||
}
|
||||
const [backgroundTasks, setBackgroundTasks] = useState<BackgroundTaskItem[]>([])
|
||||
const [selectedBackgroundTask, setSelectedBackgroundTask] = useState<string | null>(null)
|
||||
|
||||
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
|
||||
useEffect(() => {
|
||||
runIdRef.current = runId
|
||||
|
|
@ -528,12 +547,17 @@ function App() {
|
|||
const cleanup = window.ipc.on('workspace:didChange', async (event) => {
|
||||
loadDirectory().then(setTree)
|
||||
|
||||
// Reload current file if it was changed externally
|
||||
if (!selectedPath) return
|
||||
|
||||
const changedPath = event.type === 'changed' ? event.path : null
|
||||
const changedPaths = (event.type === 'bulkChanged' ? event.paths : []) ?? []
|
||||
|
||||
// Reload background tasks if agent-schedule.json changed
|
||||
if (changedPath === 'config/agent-schedule.json' || changedPaths.includes('config/agent-schedule.json')) {
|
||||
loadBackgroundTasks()
|
||||
}
|
||||
|
||||
// Reload current file if it was changed externally
|
||||
if (!selectedPath) return
|
||||
|
||||
const isCurrentFileChanged =
|
||||
changedPath === selectedPath || changedPaths.includes(selectedPath)
|
||||
|
||||
|
|
@ -548,6 +572,7 @@ function App() {
|
|||
}
|
||||
})
|
||||
return cleanup
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loadDirectory, selectedPath, editorContent])
|
||||
|
||||
// Load file content when selected
|
||||
|
|
@ -663,6 +688,63 @@ function App() {
|
|||
loadRuns()
|
||||
}, [loadRuns])
|
||||
|
||||
// Load background tasks
|
||||
const loadBackgroundTasks = useCallback(async () => {
|
||||
try {
|
||||
const [configResult, stateResult] = await Promise.all([
|
||||
window.ipc.invoke('agent-schedule:getConfig', null),
|
||||
window.ipc.invoke('agent-schedule:getState', null),
|
||||
])
|
||||
|
||||
const tasks: BackgroundTaskItem[] = Object.entries(configResult.agents).map(([name, entry]) => {
|
||||
const state = stateResult.agents[name]
|
||||
return {
|
||||
name,
|
||||
description: entry.description,
|
||||
schedule: entry.schedule,
|
||||
enabled: entry.enabled ?? true,
|
||||
startingMessage: entry.startingMessage,
|
||||
status: state?.status,
|
||||
nextRunAt: state?.nextRunAt,
|
||||
lastRunAt: state?.lastRunAt,
|
||||
lastError: state?.lastError,
|
||||
runCount: state?.runCount ?? 0,
|
||||
}
|
||||
})
|
||||
|
||||
setBackgroundTasks(tasks)
|
||||
} catch (err) {
|
||||
console.error('Failed to load background tasks:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load background tasks on mount
|
||||
useEffect(() => {
|
||||
loadBackgroundTasks()
|
||||
}, [loadBackgroundTasks])
|
||||
|
||||
// Handle toggling background task enabled state
|
||||
const handleToggleBackgroundTask = useCallback(async (taskName: string, enabled: boolean) => {
|
||||
const task = backgroundTasks.find(t => t.name === taskName)
|
||||
if (!task) return
|
||||
|
||||
try {
|
||||
await window.ipc.invoke('agent-schedule:updateAgent', {
|
||||
agentName: taskName,
|
||||
entry: {
|
||||
schedule: task.schedule,
|
||||
enabled,
|
||||
startingMessage: task.startingMessage,
|
||||
description: task.description,
|
||||
},
|
||||
})
|
||||
// Reload to get updated state
|
||||
await loadBackgroundTasks()
|
||||
} catch (err) {
|
||||
console.error('Failed to update background task:', err)
|
||||
}
|
||||
}, [backgroundTasks, loadBackgroundTasks])
|
||||
|
||||
// Load a specific run and populate conversation
|
||||
const loadRun = useCallback(async (id: string) => {
|
||||
try {
|
||||
|
|
@ -1169,6 +1251,7 @@ function App() {
|
|||
setPendingAskHumanRequests(new Map())
|
||||
setAllPermissionRequests(new Map())
|
||||
setPermissionResponses(new Map())
|
||||
setSelectedBackgroundTask(null)
|
||||
}, [])
|
||||
|
||||
const handleChatInputSubmit = (text: string) => {
|
||||
|
|
@ -1193,6 +1276,8 @@ function App() {
|
|||
// Clear forward history when navigating to a new file
|
||||
setFileHistoryForward([])
|
||||
setSelectedPath(path)
|
||||
// Clear background task selection when navigating to a file
|
||||
setSelectedBackgroundTask(null)
|
||||
}, [selectedPath])
|
||||
|
||||
const navigateBack = useCallback(() => {
|
||||
|
|
@ -1686,7 +1771,16 @@ function App() {
|
|||
const conversationContentClassName = hasConversation
|
||||
? "mx-auto w-full max-w-4xl pb-28"
|
||||
: "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0"
|
||||
const headerTitle = selectedPath ? selectedPath : (isGraphOpen ? 'Graph View' : 'Chat')
|
||||
const headerTitle = selectedPath
|
||||
? selectedPath
|
||||
: isGraphOpen
|
||||
? 'Graph View'
|
||||
: selectedBackgroundTask
|
||||
? `Background Task: ${selectedBackgroundTask}`
|
||||
: 'Chat'
|
||||
const selectedTask = selectedBackgroundTask
|
||||
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
||||
: null
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
|
|
@ -1716,8 +1810,18 @@ function App() {
|
|||
currentRunId={runId}
|
||||
tasksActions={{
|
||||
onNewChat: handleNewChat,
|
||||
onSelectRun: loadRun,
|
||||
onSelectRun: (runIdToLoad) => {
|
||||
setSelectedBackgroundTask(null)
|
||||
loadRun(runIdToLoad)
|
||||
},
|
||||
onSelectBackgroundTask: (taskName) => {
|
||||
setSelectedBackgroundTask(taskName)
|
||||
setSelectedPath(null)
|
||||
setIsGraphOpen(false)
|
||||
},
|
||||
}}
|
||||
backgroundTasks={backgroundTasks}
|
||||
selectedBackgroundTask={selectedBackgroundTask}
|
||||
/>
|
||||
<SidebarInset className="overflow-hidden! min-h-0">
|
||||
{/* Header with sidebar triggers */}
|
||||
|
|
@ -1819,6 +1923,21 @@ function App() {
|
|||
</pre>
|
||||
</div>
|
||||
)
|
||||
) : selectedTask ? (
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<BackgroundTaskDetail
|
||||
name={selectedTask.name}
|
||||
description={selectedTask.description}
|
||||
schedule={selectedTask.schedule}
|
||||
enabled={selectedTask.enabled}
|
||||
status={selectedTask.status}
|
||||
nextRunAt={selectedTask.nextRunAt}
|
||||
lastRunAt={selectedTask.lastRunAt}
|
||||
lastError={selectedTask.lastError}
|
||||
runCount={selectedTask.runCount}
|
||||
onToggleEnabled={(enabled) => handleToggleBackgroundTask(selectedTask.name, enabled)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<Conversation className="relative flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
||||
|
|
|
|||
175
apps/x/apps/renderer/src/components/background-task-detail.tsx
Normal file
175
apps/x/apps/renderer/src/components/background-task-detail.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { Bot, Calendar, Clock, AlertCircle, CheckCircle } from "lucide-react"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
|
||||
interface BackgroundTaskSchedule {
|
||||
type: "cron" | "window" | "once"
|
||||
expression?: string
|
||||
cron?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
runAt?: string
|
||||
}
|
||||
|
||||
interface BackgroundTaskDetailProps {
|
||||
name: string
|
||||
description?: string
|
||||
schedule: BackgroundTaskSchedule
|
||||
enabled: boolean
|
||||
status?: "scheduled" | "running" | "finished" | "failed" | "triggered"
|
||||
nextRunAt?: string | null
|
||||
lastRunAt?: string | null
|
||||
lastError?: string | null
|
||||
runCount?: number
|
||||
onToggleEnabled: (enabled: boolean) => void
|
||||
}
|
||||
|
||||
function formatScheduleDescription(schedule: BackgroundTaskSchedule): string {
|
||||
switch (schedule.type) {
|
||||
case "cron":
|
||||
return `Runs on cron schedule: ${schedule.expression}`
|
||||
case "window":
|
||||
return `Runs once between ${schedule.startTime} and ${schedule.endTime} based on: ${schedule.cron}`
|
||||
case "once":
|
||||
return `Runs once at ${schedule.runAt}`
|
||||
default:
|
||||
return "Unknown schedule type"
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(isoString: string | null | undefined): string {
|
||||
if (!isoString) return "Never"
|
||||
try {
|
||||
const date = new Date(isoString)
|
||||
return date.toLocaleString()
|
||||
} catch {
|
||||
return isoString
|
||||
}
|
||||
}
|
||||
|
||||
export function BackgroundTaskDetail({
|
||||
name,
|
||||
description,
|
||||
schedule,
|
||||
enabled,
|
||||
status,
|
||||
nextRunAt,
|
||||
lastRunAt,
|
||||
lastError,
|
||||
runCount = 0,
|
||||
onToggleEnabled,
|
||||
}: BackgroundTaskDetailProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center size-10 rounded-lg bg-primary/10">
|
||||
<Bot className="size-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-xl font-semibold truncate">{name}</h1>
|
||||
<p className="text-sm text-muted-foreground">Background Agent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-muted-foreground mb-2">Description</h2>
|
||||
<p className="text-sm">{description}</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Schedule */}
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-muted-foreground mb-2">Schedule</h2>
|
||||
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium capitalize">{schedule.type} Schedule</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatScheduleDescription(schedule)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Enabled Toggle - hide for completed one-time schedules */}
|
||||
{status === "triggered" ? (
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-muted-foreground mb-2">Status</h2>
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="size-4 text-green-500" />
|
||||
<p className="text-sm font-medium">Completed</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
This one-time agent has finished running and will not run again.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-muted-foreground mb-2">Status</h2>
|
||||
<div className="flex items-center justify-between bg-muted/50 rounded-lg p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{enabled ? "Enabled" : "Disabled"}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{enabled ? "This agent will run according to its schedule" : "This agent is paused and will not run"}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={onToggleEnabled}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Run Statistics */}
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-muted-foreground mb-2">Run History</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<p className="text-2xl font-semibold">{runCount}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Runs</p>
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<p className="text-sm font-medium">{formatDateTime(lastRunAt)}</p>
|
||||
<p className="text-xs text-muted-foreground">Last Run</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Next Run */}
|
||||
{nextRunAt && schedule.type !== "once" && (
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-muted-foreground mb-2">Next Scheduled Run</h2>
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm">{formatDateTime(nextRunAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Last Error */}
|
||||
{lastError && (
|
||||
<section>
|
||||
<h2 className="text-sm font-medium text-red-500 mb-2">Last Error</h2>
|
||||
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="size-4 text-red-500 mt-0.5 shrink-0" />
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{lastError}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import * as React from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Bot,
|
||||
ChevronRight,
|
||||
ChevronsDownUp,
|
||||
ChevronsUpDown,
|
||||
|
|
@ -78,9 +79,27 @@ type RunListItem = {
|
|||
agentId: string
|
||||
}
|
||||
|
||||
type BackgroundTaskItem = {
|
||||
name: string
|
||||
description?: string
|
||||
schedule: {
|
||||
type: "cron" | "window" | "once"
|
||||
expression?: string
|
||||
cron?: string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
runAt?: string
|
||||
}
|
||||
enabled: boolean
|
||||
status?: "scheduled" | "running" | "finished" | "failed" | "triggered"
|
||||
nextRunAt?: string | null
|
||||
lastRunAt?: string | null
|
||||
}
|
||||
|
||||
type TasksActions = {
|
||||
onNewChat: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
onSelectBackgroundTask?: (taskName: string) => void
|
||||
}
|
||||
|
||||
type SidebarContentPanelProps = {
|
||||
|
|
@ -93,6 +112,8 @@ type SidebarContentPanelProps = {
|
|||
runs?: RunListItem[]
|
||||
currentRunId?: string | null
|
||||
tasksActions?: TasksActions
|
||||
backgroundTasks?: BackgroundTaskItem[]
|
||||
selectedBackgroundTask?: string | null
|
||||
} & React.ComponentProps<typeof Sidebar>
|
||||
|
||||
const sectionTitles = {
|
||||
|
|
@ -110,6 +131,8 @@ export function SidebarContentPanel({
|
|||
runs = [],
|
||||
currentRunId,
|
||||
tasksActions,
|
||||
backgroundTasks = [],
|
||||
selectedBackgroundTask,
|
||||
...props
|
||||
}: SidebarContentPanelProps) {
|
||||
const { activeSection } = useSidebarSection()
|
||||
|
|
@ -137,6 +160,8 @@ export function SidebarContentPanel({
|
|||
runs={runs}
|
||||
currentRunId={currentRunId}
|
||||
actions={tasksActions}
|
||||
backgroundTasks={backgroundTasks}
|
||||
selectedBackgroundTask={selectedBackgroundTask}
|
||||
/>
|
||||
)}
|
||||
</SidebarContent>
|
||||
|
|
@ -653,15 +678,40 @@ function Tree({
|
|||
)
|
||||
}
|
||||
|
||||
// Get status indicator color
|
||||
function getStatusColor(status?: string, enabled?: boolean): string {
|
||||
// Disabled agents always show gray
|
||||
if (enabled === false) {
|
||||
return "bg-gray-400"
|
||||
}
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "bg-blue-500"
|
||||
case "finished":
|
||||
return "bg-green-500"
|
||||
case "failed":
|
||||
return "bg-red-500"
|
||||
case "triggered":
|
||||
return "bg-gray-400"
|
||||
case "scheduled":
|
||||
default:
|
||||
return "bg-yellow-500"
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks Section
|
||||
function TasksSection({
|
||||
runs,
|
||||
currentRunId,
|
||||
actions,
|
||||
backgroundTasks = [],
|
||||
selectedBackgroundTask,
|
||||
}: {
|
||||
runs: RunListItem[]
|
||||
currentRunId?: string | null
|
||||
actions?: TasksActions
|
||||
backgroundTasks?: BackgroundTaskItem[]
|
||||
selectedBackgroundTask?: string | null
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
||||
|
|
@ -677,9 +727,38 @@ function TasksSection({
|
|||
</SidebarMenu>
|
||||
</div>
|
||||
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
||||
{runs.length > 0 && (
|
||||
{/* Background Tasks Section */}
|
||||
{backgroundTasks.length > 0 && (
|
||||
<>
|
||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
Background Tasks
|
||||
</div>
|
||||
<SidebarMenu>
|
||||
{backgroundTasks.map((task) => (
|
||||
<SidebarMenuItem key={task.name}>
|
||||
<SidebarMenuButton
|
||||
isActive={selectedBackgroundTask === task.name}
|
||||
onClick={() => actions?.onSelectBackgroundTask?.(task.name)}
|
||||
className="gap-2"
|
||||
>
|
||||
<div className="relative">
|
||||
<Bot className="size-4 shrink-0" />
|
||||
<span
|
||||
className={`absolute -bottom-0.5 -right-0.5 size-2 rounded-full ${getStatusColor(task.status, task.enabled)} ${task.status === "running" && task.enabled ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<span className={`truncate text-sm ${!task.enabled ? "text-muted-foreground" : ""}`}>
|
||||
{task.name}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</>
|
||||
)}
|
||||
{runs.length > 0 && (
|
||||
<>
|
||||
<div className="px-3 py-1.5 mt-4 text-xs font-medium text-muted-foreground">
|
||||
Chat history
|
||||
</div>
|
||||
<SidebarMenu>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue