mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
ui 1
This commit is contained in:
parent
d59c8d53c6
commit
35805068c0
6 changed files with 432 additions and 5 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 { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
||||||
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
||||||
import * as composioHandler from './composio-handler.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 InvokeChannels = ipc.InvokeChannels;
|
||||||
type IPCChannels = ipc.IPCChannels;
|
type IPCChannels = ipc.IPCChannels;
|
||||||
|
|
@ -384,5 +387,30 @@ export function setupIpcHandlers() {
|
||||||
'composio:execute-action': async (_event, args) => {
|
'composio:execute-action': async (_event, args) => {
|
||||||
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
|
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
|
||||||
},
|
},
|
||||||
|
// Agent schedule handlers
|
||||||
|
'agent-schedule:getConfig': async () => {
|
||||||
|
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||||
|
await repo.ensureConfig();
|
||||||
|
return repo.getConfig();
|
||||||
|
},
|
||||||
|
'agent-schedule:getState': async () => {
|
||||||
|
const repo = container.resolve<IAgentScheduleStateRepo>('agentScheduleStateRepo');
|
||||||
|
await repo.ensureState();
|
||||||
|
return repo.getState();
|
||||||
|
},
|
||||||
|
'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 };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ import { Separator } from "@/components/ui/separator"
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
||||||
import { OnboardingModal } from '@/components/onboarding-modal'
|
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 DirEntry = z.infer<typeof workspace.DirEntry>
|
||||||
type RunEventType = z.infer<typeof RunEvent>
|
type RunEventType = z.infer<typeof RunEvent>
|
||||||
|
|
@ -499,6 +502,22 @@ function App() {
|
||||||
// Onboarding state
|
// Onboarding state
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
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)
|
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runIdRef.current = runId
|
runIdRef.current = runId
|
||||||
|
|
@ -663,6 +682,63 @@ function App() {
|
||||||
loadRuns()
|
loadRuns()
|
||||||
}, [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
|
// Load a specific run and populate conversation
|
||||||
const loadRun = useCallback(async (id: string) => {
|
const loadRun = useCallback(async (id: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1169,6 +1245,7 @@ function App() {
|
||||||
setPendingAskHumanRequests(new Map())
|
setPendingAskHumanRequests(new Map())
|
||||||
setAllPermissionRequests(new Map())
|
setAllPermissionRequests(new Map())
|
||||||
setPermissionResponses(new Map())
|
setPermissionResponses(new Map())
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleChatInputSubmit = (text: string) => {
|
const handleChatInputSubmit = (text: string) => {
|
||||||
|
|
@ -1193,6 +1270,8 @@ function App() {
|
||||||
// Clear forward history when navigating to a new file
|
// Clear forward history when navigating to a new file
|
||||||
setFileHistoryForward([])
|
setFileHistoryForward([])
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
|
// Clear background task selection when navigating to a file
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
}, [selectedPath])
|
}, [selectedPath])
|
||||||
|
|
||||||
const navigateBack = useCallback(() => {
|
const navigateBack = useCallback(() => {
|
||||||
|
|
@ -1686,7 +1765,16 @@ function App() {
|
||||||
const conversationContentClassName = hasConversation
|
const conversationContentClassName = hasConversation
|
||||||
? "mx-auto w-full max-w-4xl pb-28"
|
? "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"
|
: "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 (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
|
|
@ -1716,8 +1804,18 @@ function App() {
|
||||||
currentRunId={runId}
|
currentRunId={runId}
|
||||||
tasksActions={{
|
tasksActions={{
|
||||||
onNewChat: handleNewChat,
|
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">
|
<SidebarInset className="overflow-hidden! min-h-0">
|
||||||
{/* Header with sidebar triggers */}
|
{/* Header with sidebar triggers */}
|
||||||
|
|
@ -1819,6 +1917,21 @@ function App() {
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</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">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<Conversation className="relative flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
<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 * as React from "react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
|
Bot,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsDownUp,
|
ChevronsDownUp,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
|
|
@ -78,9 +79,27 @@ type RunListItem = {
|
||||||
agentId: string
|
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 = {
|
type TasksActions = {
|
||||||
onNewChat: () => void
|
onNewChat: () => void
|
||||||
onSelectRun: (runId: string) => void
|
onSelectRun: (runId: string) => void
|
||||||
|
onSelectBackgroundTask?: (taskName: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarContentPanelProps = {
|
type SidebarContentPanelProps = {
|
||||||
|
|
@ -93,6 +112,8 @@ type SidebarContentPanelProps = {
|
||||||
runs?: RunListItem[]
|
runs?: RunListItem[]
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
tasksActions?: TasksActions
|
tasksActions?: TasksActions
|
||||||
|
backgroundTasks?: BackgroundTaskItem[]
|
||||||
|
selectedBackgroundTask?: string | null
|
||||||
} & React.ComponentProps<typeof Sidebar>
|
} & React.ComponentProps<typeof Sidebar>
|
||||||
|
|
||||||
const sectionTitles = {
|
const sectionTitles = {
|
||||||
|
|
@ -110,6 +131,8 @@ export function SidebarContentPanel({
|
||||||
runs = [],
|
runs = [],
|
||||||
currentRunId,
|
currentRunId,
|
||||||
tasksActions,
|
tasksActions,
|
||||||
|
backgroundTasks = [],
|
||||||
|
selectedBackgroundTask,
|
||||||
...props
|
...props
|
||||||
}: SidebarContentPanelProps) {
|
}: SidebarContentPanelProps) {
|
||||||
const { activeSection } = useSidebarSection()
|
const { activeSection } = useSidebarSection()
|
||||||
|
|
@ -137,6 +160,8 @@ export function SidebarContentPanel({
|
||||||
runs={runs}
|
runs={runs}
|
||||||
currentRunId={currentRunId}
|
currentRunId={currentRunId}
|
||||||
actions={tasksActions}
|
actions={tasksActions}
|
||||||
|
backgroundTasks={backgroundTasks}
|
||||||
|
selectedBackgroundTask={selectedBackgroundTask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</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
|
// Tasks Section
|
||||||
function TasksSection({
|
function TasksSection({
|
||||||
runs,
|
runs,
|
||||||
currentRunId,
|
currentRunId,
|
||||||
actions,
|
actions,
|
||||||
|
backgroundTasks = [],
|
||||||
|
selectedBackgroundTask,
|
||||||
}: {
|
}: {
|
||||||
runs: RunListItem[]
|
runs: RunListItem[]
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
actions?: TasksActions
|
actions?: TasksActions
|
||||||
|
backgroundTasks?: BackgroundTaskItem[]
|
||||||
|
selectedBackgroundTask?: string | null
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
|
@ -677,6 +727,35 @@ function TasksSection({
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</div>
|
</div>
|
||||||
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
||||||
|
{/* 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 && (
|
{runs.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -146,15 +146,19 @@ You can add a ` + "`description`" + ` field to describe what the agent does. Thi
|
||||||
}
|
}
|
||||||
` + "```" + `
|
` + "```" + `
|
||||||
|
|
||||||
### Schedule State
|
### Schedule State (Read-Only)
|
||||||
|
|
||||||
The runner tracks execution state in ` + "`~/.rowboat/config/agent-schedule-state.json`" + `:
|
**IMPORTANT: Do NOT modify ` + "`agent-schedule-state.json`" + `** - it is managed automatically by the background runner.
|
||||||
|
|
||||||
|
The runner automatically tracks execution state in ` + "`~/.rowboat/config/agent-schedule-state.json`" + `:
|
||||||
- ` + "`status`" + `: scheduled, running, finished, failed, triggered (for once-schedules)
|
- ` + "`status`" + `: scheduled, running, finished, failed, triggered (for once-schedules)
|
||||||
- ` + "`lastRunAt`" + `: When the agent last ran
|
- ` + "`lastRunAt`" + `: When the agent last ran
|
||||||
- ` + "`nextRunAt`" + `: When the agent will run next
|
- ` + "`nextRunAt`" + `: When the agent will run next
|
||||||
- ` + "`lastError`" + `: Error message if the last run failed
|
- ` + "`lastError`" + `: Error message if the last run failed
|
||||||
- ` + "`runCount`" + `: Total number of runs
|
- ` + "`runCount`" + `: Total number of runs
|
||||||
|
|
||||||
|
When you add an agent to ` + "`agent-schedule.json`" + `, the runner will automatically create and manage its state entry. You only need to edit ` + "`agent-schedule.json`" + `.
|
||||||
|
|
||||||
## Agent File Format
|
## Agent File Format
|
||||||
|
|
||||||
Agent files are **Markdown files with YAML frontmatter**. The frontmatter contains configuration (model, tools), and the body contains the instructions.
|
Agent files are **Markdown files with YAML frontmatter**. The frontmatter contains configuration (model, tools), and the body contains the instructions.
|
||||||
|
|
@ -544,7 +548,7 @@ Use the search tool to find information on the web.
|
||||||
5. When creating multi-agent workflows, create an orchestrator agent
|
5. When creating multi-agent workflows, create an orchestrator agent
|
||||||
6. Add other agents as tools with ` + "`type: agent`" + ` for chaining
|
6. Add other agents as tools with ` + "`type: agent`" + ` for chaining
|
||||||
7. Use ` + "`listMcpServers`" + ` and ` + "`listMcpTools`" + ` when adding MCP integrations
|
7. Use ` + "`listMcpServers`" + ` and ` + "`listMcpTools`" + ` when adding MCP integrations
|
||||||
8. Configure schedules in ` + "`~/.rowboat/config/agent-schedule.json`" + `
|
8. Configure schedules in ` + "`~/.rowboat/config/agent-schedule.json`" + ` (ONLY edit this file, NOT the state file)
|
||||||
9. Confirm work done and outline next steps once changes are complete
|
9. Confirm work done and outline next steps once changes are complete
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { RelPath, Encoding, Stat, DirEntry, ReaddirOptions, ReadFileResult, Work
|
||||||
import { ListToolsResponse } from './mcp.js';
|
import { ListToolsResponse } from './mcp.js';
|
||||||
import { AskHumanResponsePayload, CreateRunOptions, Run, ListRunsResponse, ToolPermissionAuthorizePayload } from './runs.js';
|
import { AskHumanResponsePayload, CreateRunOptions, Run, ListRunsResponse, ToolPermissionAuthorizePayload } from './runs.js';
|
||||||
import { LlmModelConfig } from './models.js';
|
import { LlmModelConfig } from './models.js';
|
||||||
|
import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js';
|
||||||
|
import { AgentScheduleState } from './agent-schedule-state.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Runtime Validation Schemas (Single Source of Truth)
|
// Runtime Validation Schemas (Single Source of Truth)
|
||||||
|
|
@ -353,6 +355,32 @@ const ipcSchemas = {
|
||||||
}),
|
}),
|
||||||
res: z.null(),
|
res: z.null(),
|
||||||
},
|
},
|
||||||
|
// Agent schedule channels
|
||||||
|
'agent-schedule:getConfig': {
|
||||||
|
req: z.null(),
|
||||||
|
res: AgentScheduleConfig,
|
||||||
|
},
|
||||||
|
'agent-schedule:getState': {
|
||||||
|
req: z.null(),
|
||||||
|
res: AgentScheduleState,
|
||||||
|
},
|
||||||
|
'agent-schedule:updateAgent': {
|
||||||
|
req: z.object({
|
||||||
|
agentName: z.string(),
|
||||||
|
entry: AgentScheduleEntry,
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'agent-schedule:deleteAgent': {
|
||||||
|
req: z.object({
|
||||||
|
agentName: z.string(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
}),
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue