mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
move tasks to main side bar
This commit is contained in:
parent
f8100d353d
commit
2da914b6e7
3 changed files with 134 additions and 98 deletions
|
|
@ -1402,6 +1402,13 @@ function App() {
|
|||
loadBackgroundTasks()
|
||||
}
|
||||
|
||||
// Reload bg-task summaries if anything under bg-tasks/ changed
|
||||
if (
|
||||
eventPaths.some((p) => p === 'bg-tasks' || p.startsWith('bg-tasks/'))
|
||||
) {
|
||||
loadBgTaskSummaries()
|
||||
}
|
||||
|
||||
// Invalidate cached content for files changed outside the active editor.
|
||||
// This prevents stale backlinks after rename-rewrite passes touch many files.
|
||||
for (const path of eventPaths) {
|
||||
|
|
@ -1747,6 +1754,37 @@ function App() {
|
|||
loadRuns()
|
||||
}, [loadRuns])
|
||||
|
||||
const [bgTaskSummaries, setBgTaskSummaries] = useState<Array<{
|
||||
slug: string
|
||||
name: string
|
||||
active: boolean
|
||||
createdAt: string
|
||||
lastAttemptAt?: string
|
||||
lastRunAt?: string
|
||||
}>>([])
|
||||
const [bgTaskInitialSlug, setBgTaskInitialSlug] = useState<string | null>(null)
|
||||
const [bgTaskSlugVersion, setBgTaskSlugVersion] = useState(0)
|
||||
|
||||
const loadBgTaskSummaries = useCallback(async () => {
|
||||
try {
|
||||
const result = await window.ipc.invoke('bg-task:list', { limit: 200 })
|
||||
setBgTaskSummaries(result.items.map((it) => ({
|
||||
slug: it.slug,
|
||||
name: it.name,
|
||||
active: it.active,
|
||||
createdAt: it.createdAt,
|
||||
lastAttemptAt: it.lastAttemptAt,
|
||||
lastRunAt: it.lastRunAt,
|
||||
})))
|
||||
} catch (err) {
|
||||
console.error('Failed to load bg-task summaries:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadBgTaskSummaries()
|
||||
}, [loadBgTaskSummaries])
|
||||
|
||||
// Load background tasks
|
||||
const loadBackgroundTasks = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -5069,8 +5107,12 @@ function App() {
|
|||
void navigateToView({ type: 'chat-history' })
|
||||
},
|
||||
}}
|
||||
backgroundTasks={backgroundTasks}
|
||||
selectedBackgroundTask={selectedBackgroundTask}
|
||||
bgTaskSummaries={bgTaskSummaries}
|
||||
onOpenBgTask={(slug) => {
|
||||
setBgTaskInitialSlug(slug)
|
||||
setBgTaskSlugVersion((v) => v + 1)
|
||||
openBgTasksView()
|
||||
}}
|
||||
isSearchOpen={isSearchOpen}
|
||||
isBrowserOpen={isBrowserOpen}
|
||||
onToggleBrowser={handleToggleBrowser}
|
||||
|
|
@ -5078,8 +5120,7 @@ function App() {
|
|||
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
|
||||
isMeetingsOpen={isMeetingsOpen}
|
||||
onOpenMeetings={openMeetingsView}
|
||||
isBgTasksOpen={isBgTasksOpen}
|
||||
onOpenBgTasks={openBgTasksView}
|
||||
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
||||
isEmailOpen={isEmailOpen}
|
||||
onOpenEmail={openEmailView}
|
||||
/>
|
||||
|
|
@ -5246,6 +5287,8 @@ function App() {
|
|||
) : isBgTasksOpen ? (
|
||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||
<BgTasksView
|
||||
initialSlug={bgTaskInitialSlug}
|
||||
slugVersion={bgTaskSlugVersion}
|
||||
onCreateWithCopilot={(description) => {
|
||||
submitFromPalette(buildBgTaskSetupPrompt(description), null)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1419,6 +1419,18 @@ export interface BgTasksViewProps {
|
|||
* "Edit with Copilot" button in the detail-view sidebar footer.
|
||||
*/
|
||||
onEditWithCopilot?: (slug: string) => void
|
||||
/**
|
||||
* If provided, the view opens with this task already selected. Updates to
|
||||
* this prop sync into internal state so the sidebar can swap which task is
|
||||
* focused without remounting the view.
|
||||
*/
|
||||
initialSlug?: string | null
|
||||
/**
|
||||
* Bump this counter to force a re-focus on `initialSlug` even when the
|
||||
* slug value itself didn't change (e.g. user clicks the same task in the
|
||||
* sidebar twice after navigating away inside the view).
|
||||
*/
|
||||
slugVersion?: number
|
||||
}
|
||||
|
||||
function formatLastRanLabel(iso: string | null | undefined): string {
|
||||
|
|
@ -1426,9 +1438,12 @@ function formatLastRanLabel(iso: string | null | undefined): string {
|
|||
return formatRelativeTime(iso) || 'Never'
|
||||
}
|
||||
|
||||
export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot }: BgTasksViewProps = {}) {
|
||||
export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot, initialSlug, slugVersion }: BgTasksViewProps = {}) {
|
||||
const [items, setItems] = useState<BackgroundTaskSummary[]>([])
|
||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null)
|
||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(initialSlug ?? null)
|
||||
useEffect(() => {
|
||||
setSelectedSlug(initialSlug ?? null)
|
||||
}, [initialSlug, slugVersion])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showNewDialog, setShowNewDialog] = useState(false)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
Mic,
|
||||
Plug,
|
||||
Lightbulb,
|
||||
ListChecks,
|
||||
LoaderIcon,
|
||||
Mail,
|
||||
Settings,
|
||||
|
|
@ -124,21 +123,13 @@ type RunListItem = {
|
|||
agentId: string
|
||||
}
|
||||
|
||||
type BackgroundTaskItem = {
|
||||
type TaskSummary = {
|
||||
slug: string
|
||||
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
|
||||
active: boolean
|
||||
createdAt: string
|
||||
lastAttemptAt?: string
|
||||
lastRunAt?: string
|
||||
}
|
||||
|
||||
type ServiceEventType = z.infer<typeof ServiceEvent>
|
||||
|
|
@ -195,8 +186,8 @@ type SidebarContentPanelProps = {
|
|||
currentRunId?: string | null
|
||||
processingRunIds?: Set<string>
|
||||
tasksActions?: TasksActions
|
||||
backgroundTasks?: BackgroundTaskItem[]
|
||||
selectedBackgroundTask?: string | null
|
||||
bgTaskSummaries?: TaskSummary[]
|
||||
onOpenBgTask?: (slug: string) => void
|
||||
isSearchOpen?: boolean
|
||||
isBrowserOpen?: boolean
|
||||
onToggleBrowser?: () => void
|
||||
|
|
@ -204,7 +195,6 @@ type SidebarContentPanelProps = {
|
|||
onOpenSuggestedTopics?: () => void
|
||||
isMeetingsOpen?: boolean
|
||||
onOpenMeetings?: () => void
|
||||
isBgTasksOpen?: boolean
|
||||
onOpenBgTasks?: () => void
|
||||
isEmailOpen?: boolean
|
||||
onOpenEmail?: () => void
|
||||
|
|
@ -444,8 +434,8 @@ export function SidebarContentPanel({
|
|||
currentRunId,
|
||||
processingRunIds,
|
||||
tasksActions,
|
||||
backgroundTasks = [],
|
||||
selectedBackgroundTask,
|
||||
bgTaskSummaries = [],
|
||||
onOpenBgTask,
|
||||
isSearchOpen = false,
|
||||
isBrowserOpen = false,
|
||||
onToggleBrowser,
|
||||
|
|
@ -453,7 +443,6 @@ export function SidebarContentPanel({
|
|||
onOpenSuggestedTopics,
|
||||
isMeetingsOpen = false,
|
||||
onOpenMeetings,
|
||||
isBgTasksOpen = false,
|
||||
onOpenBgTasks,
|
||||
isEmailOpen = false,
|
||||
onOpenEmail,
|
||||
|
|
@ -471,7 +460,6 @@ export function SidebarContentPanel({
|
|||
const isBrowserQuickActionSelected = isBrowserOpen && !isSearchOpen
|
||||
const isSuggestedTopicsQuickActionSelected = isSuggestedTopicsOpen && !isBrowserOpen
|
||||
const isMeetingsQuickActionSelected = isMeetingsOpen && !isBrowserOpen
|
||||
const isBgTasksQuickActionSelected = isBgTasksOpen && !isBrowserOpen
|
||||
const isEmailQuickActionSelected = isEmailOpen && !isBrowserOpen
|
||||
|
||||
const handleRowboatLogin = useCallback(async () => {
|
||||
|
|
@ -567,24 +555,14 @@ export function SidebarContentPanel({
|
|||
<span>Meetings</span>
|
||||
</button>
|
||||
)}
|
||||
{onOpenBgTasks && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenBgTasks}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
||||
isBgTasksQuickActionSelected
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<ListChecks className="size-4" />
|
||||
<span>Agents</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<TasksSidebarSection
|
||||
tasks={bgTaskSummaries}
|
||||
onOpenTask={onOpenBgTask}
|
||||
onOpenTasksView={onOpenBgTasks}
|
||||
/>
|
||||
<KnowledgeSection
|
||||
tree={tree}
|
||||
selectedPath={selectedPath}
|
||||
|
|
@ -597,8 +575,6 @@ export function SidebarContentPanel({
|
|||
currentRunId={currentRunId}
|
||||
processingRunIds={processingRunIds}
|
||||
actions={tasksActions}
|
||||
backgroundTasks={backgroundTasks}
|
||||
selectedBackgroundTask={selectedBackgroundTask}
|
||||
/>
|
||||
</SidebarContent>
|
||||
{/* Billing / upgrade CTA or Log in CTA */}
|
||||
|
|
@ -1124,25 +1100,60 @@ export function WorkspaceSection({
|
|||
}
|
||||
|
||||
|
||||
// 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"
|
||||
}
|
||||
function TasksSidebarSection({
|
||||
tasks,
|
||||
onOpenTask,
|
||||
onOpenTasksView,
|
||||
}: {
|
||||
tasks: TaskSummary[]
|
||||
onOpenTask?: (slug: string) => void
|
||||
onOpenTasksView?: () => void
|
||||
}) {
|
||||
const recentTasks = React.useMemo<TaskSummary[]>(() => {
|
||||
const toTime = (s?: string | null): number => {
|
||||
if (!s) return 0
|
||||
const t = new Date(s).getTime()
|
||||
return Number.isNaN(t) ? 0 : t
|
||||
}
|
||||
const activity = (t: TaskSummary): number =>
|
||||
Math.max(toTime(t.lastRunAt), toTime(t.lastAttemptAt), toTime(t.createdAt))
|
||||
return [...tasks]
|
||||
.sort((a, b) => activity(b) - activity(a))
|
||||
.slice(0, 3)
|
||||
}, [tasks])
|
||||
|
||||
return (
|
||||
<SidebarGroup className="flex flex-col">
|
||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
Tasks
|
||||
</div>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{recentTasks.map((task) => (
|
||||
<SidebarMenuItem key={task.slug}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => onOpenTask?.(task.slug)}
|
||||
className="gap-2"
|
||||
>
|
||||
<Bot className="size-4 shrink-0" />
|
||||
<span className={`truncate text-sm ${!task.active ? "text-muted-foreground" : ""}`}>
|
||||
{task.name}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{onOpenTasksView && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={onOpenTasksView}>
|
||||
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">View all</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
|
||||
// Tasks Section
|
||||
|
|
@ -1151,51 +1162,18 @@ function TasksSection({
|
|||
currentRunId,
|
||||
processingRunIds,
|
||||
actions,
|
||||
backgroundTasks = [],
|
||||
selectedBackgroundTask,
|
||||
}: {
|
||||
runs: RunListItem[]
|
||||
currentRunId?: string | null
|
||||
processingRunIds?: Set<string>
|
||||
actions?: TasksActions
|
||||
backgroundTasks?: BackgroundTaskItem[]
|
||||
selectedBackgroundTask?: string | null
|
||||
}) {
|
||||
const [pendingDeleteRunId, setPendingDeleteRunId] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<SidebarGroup className="flex flex-col">
|
||||
<SidebarGroupContent>
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
<div className="px-3 py-1.5 mt-4 text-xs font-medium text-muted-foreground">
|
||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
Chat history
|
||||
</div>
|
||||
<SidebarMenu>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue