From 574cd31912478775a107ad95e9de933f4e2cc7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CENK=20TEK=C4=B0N?= <143123890+cenktekin@users.noreply.github.com> Date: Thu, 7 May 2026 21:02:33 +0300 Subject: [PATCH 1/5] feat(agent): group scheduled task results by task name with collapse/expand - Add ScheduledTaskResultGroup component for accordion-style grouping - Group results by job name with expand/collapse behavior - Only one group expanded at a time (accordion pattern) - Update ScheduleResults (newtab) for consistency - Each group shows: task name, latest timestamp, result count Fixes #950 --- .../ScheduledTaskResultGroup.tsx | 162 ++++++++++++++ .../scheduled-tasks/ScheduledTaskResults.tsx | 133 ++++-------- .../newtab/index/ScheduleResults.tsx | 204 ++++++++++++------ 3 files changed, 341 insertions(+), 158 deletions(-) create mode 100644 packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx new file mode 100644 index 000000000..07b2e8747 --- /dev/null +++ b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx @@ -0,0 +1,162 @@ +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import { + CheckCircle2, + ChevronDown, + Clock, + Loader2, + RotateCcw, + Square, + XCircle, +} from 'lucide-react' +import type { FC } from 'react' +import { Button } from '@/components/ui/button' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' +import type { ScheduledJob, ScheduledJobRun } from './types' + +dayjs.extend(relativeTime) + +interface JobRunWithDetails extends ScheduledJobRun { + job: ScheduledJob | undefined +} + +interface ScheduledTaskResultGroupProps { + job: ScheduledJob + runs: JobRunWithDetails[] + isOpen: boolean + onOpenChange: (open: boolean) => void + onViewRun: (run: ScheduledJobRun) => void + onCancelRun: (runId: string) => void + onRetryRun: (jobId: string) => void +} + +const getStatusIcon = (status: JobRunWithDetails['status']) => { + switch (status) { + case 'completed': + return + case 'running': + return + case 'failed': + return + } +} + +const formatTimestamp = (dateString: string) => dayjs(dateString).fromNow() + +export const ScheduledTaskResultGroup: FC = ({ + job, + runs, + isOpen, + onOpenChange, + onViewRun, + onCancelRun, + onRetryRun, +}) => { + const latestRun = runs[0] + const latestTime = latestRun ? formatTimestamp(latestRun.startedAt) : '' + const runningCount = runs.filter((r) => r.status === 'running').length + + return ( + + + + + + +
+ {runs.map((run) => ( + + )} + {run.status === 'failed' && ( + + )} +
+ + ))} + +
+
+ ) +} diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResults.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResults.tsx index 8e02ad7ce..a5cd378ad 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResults.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResults.tsx @@ -1,17 +1,6 @@ -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' -import { - Calendar, - CheckCircle2, - Clock, - Loader2, - RotateCcw, - Square, - XCircle, -} from 'lucide-react' +import { Calendar } from 'lucide-react' import type { FC } from 'react' -import { useMemo } from 'react' -import { Button } from '@/components/ui/button' +import { useMemo, useState } from 'react' import { useScheduledJobRuns, useScheduledJobs, @@ -20,8 +9,7 @@ import type { ScheduledJob, ScheduledJobRun, } from '@/lib/schedules/scheduleTypes' - -dayjs.extend(relativeTime) +import { ScheduledTaskResultGroup } from './ScheduledTaskResultGroup' interface JobRunWithDetails extends ScheduledJobRun { job: ScheduledJob | undefined @@ -33,19 +21,6 @@ interface ScheduledTaskResultsProps { onRetryRun: (jobId: string) => void } -const getStatusIcon = (status: JobRunWithDetails['status']) => { - switch (status) { - case 'completed': - return - case 'running': - return - case 'failed': - return - } -} - -const formatTimestamp = (dateString: string) => dayjs(dateString).fromNow() - export const ScheduledTaskResults: FC = ({ onViewRun, onCancelRun, @@ -53,29 +28,45 @@ export const ScheduledTaskResults: FC = ({ }) => { const { jobRuns } = useScheduledJobRuns() const { jobs } = useScheduledJobs() + const [openGroupId, setOpenGroupId] = useState(null) - const sortedRuns: JobRunWithDetails[] = useMemo(() => { + const groupedRuns = useMemo(() => { const enrichWithJob = (run: ScheduledJobRun): JobRunWithDetails => ({ ...run, job: jobs.find((j) => j.id === run.jobId), }) - const running = jobRuns - .filter((r) => r.status === 'running') - .map(enrichWithJob) + const runsByJob = new Map() - const completedOrFailed = jobRuns - .filter((r) => r.status === 'completed' || r.status === 'failed') - .sort( + for (const run of jobRuns) { + const enriched = enrichWithJob(run) + const existing = runsByJob.get(run.jobId) ?? [] + existing.push(enriched) + runsByJob.set(run.jobId, existing) + } + + const groups: Array<{ job: ScheduledJob; runs: JobRunWithDetails[] }> = [] + + for (const [jobId, runs] of runsByJob) { + const job = jobs.find((j) => j.id === jobId) + if (!job) continue + + const sorted = runs.sort( (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(), ) - .map(enrichWithJob) - return [...running, ...completedOrFailed] + groups.push({ job, runs: sorted }) + } + + return groups.sort((a, b) => { + const latestA = a.runs[0]?.startedAt ?? '' + const latestB = b.runs[0]?.startedAt ?? '' + return new Date(latestB).getTime() - new Date(latestA).getTime() + }) }, [jobRuns, jobs]) - if (!sortedRuns.length) { + if (!groupedRuns.length) { return (
@@ -86,61 +77,17 @@ export const ScheduledTaskResults: FC = ({ return (
- {sortedRuns.map((run) => ( - - )} - {run.status === 'failed' && ( - - )} -
- + {groupedRuns.map(({ job, runs }) => ( + setOpenGroupId(open ? job.id : null)} + onViewRun={onViewRun} + onCancelRun={onCancelRun} + onRetryRun={onRetryRun} + /> ))}
) diff --git a/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx b/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx index 82ab09df7..83ffccbef 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx @@ -42,6 +42,11 @@ interface JobRunWithDetails extends ScheduledJobRun { job: ScheduledJob | undefined } +interface JobGroup { + job: ScheduledJob + runs: JobRunWithDetails[] +} + const MAX_DISPLAY_COUNT = 3 const SCHEDULE_RESULTS_COLLAPSED_KEY = 'schedule-results-collapsed' @@ -63,6 +68,7 @@ export const ScheduleResults: FC = () => { const stored = localStorage.getItem(SCHEDULE_RESULTS_COLLAPSED_KEY) return stored !== 'true' }) + const [openGroupId, setOpenGroupId] = useState(null) const [viewingRun, setViewingRun] = useState(null) const handleOpenChange = (open: boolean) => { @@ -75,30 +81,42 @@ export const ScheduleResults: FC = () => { const runningCount = jobRuns.filter((r) => r.status === 'running').length - const displayedRuns: JobRunWithDetails[] = useMemo(() => { + const groupedRuns: JobGroup[] = useMemo(() => { const enrichWithJob = (run: ScheduledJobRun): JobRunWithDetails => ({ ...run, job: jobs.find((j) => j.id === run.jobId), }) - const runningJobs = jobRuns - .filter((r) => r.status === 'running') - .map(enrichWithJob) + const runsByJob = new Map() - if (runningJobs.length >= MAX_DISPLAY_COUNT) { - return runningJobs + for (const run of jobRuns) { + const enriched = enrichWithJob(run) + const existing = runsByJob.get(run.jobId) ?? [] + existing.push(enriched) + runsByJob.set(run.jobId, existing) } - const completedOrFailed = jobRuns - .filter((r) => r.status === 'completed' || r.status === 'failed') - .sort( + const groups: JobGroup[] = [] + + for (const [jobId, runs] of runsByJob) { + const job = jobs.find((j) => j.id === jobId) + if (!job) continue + + const sorted = runs.sort( (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(), ) - .slice(0, MAX_DISPLAY_COUNT - runningJobs.length) - .map(enrichWithJob) - return [...runningJobs, ...completedOrFailed] + groups.push({ job, runs: sorted }) + } + + return groups + .sort((a, b) => { + const latestA = a.runs[0]?.startedAt ?? '' + const latestB = b.runs[0]?.startedAt ?? '' + return new Date(latestB).getTime() - new Date(latestA).getTime() + }) + .slice(0, MAX_DISPLAY_COUNT) }, [jobRuns, jobs]) const viewRun = (run: JobRunWithDetails) => { @@ -117,7 +135,7 @@ export const ScheduleResults: FC = () => { track(SCHEDULED_TASK_RETRIED_EVENT) } - if (!displayedRuns.length) return null + if (!groupedRuns.length) return null return ( { - {displayedRuns.map((run) => ( - - )} - {run.status === 'failed' && ( + {groupedRuns.map(({ job, runs }) => { + const latestRun = runs[0] + const latestTime = latestRun ? formatTimestamp(latestRun.startedAt) : '' + const groupRunningCount = runs.filter( + (r) => r.status === 'running', + ).length + + return ( + setOpenGroupId(open ? job.id : null)} + > + - )} - - - ))} + + + +
+ {runs.map((run) => ( + + )} + {run.status === 'failed' && ( + + )} +
+ + ))} + +
+
+ ) + })} )} + {run.result && ( + + + + + + handleContinueInChat('chat')}> + + Chat + + handleContinueInChat('agent')}> + + Assistant + + + + )} diff --git a/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx b/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx index d81e6db8d..f04cb7c1b 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/newtab/index/ScheduleResults.tsx @@ -35,7 +35,7 @@ import { groupRunsByJob, type JobGroup, type JobRunWithDetails, -} from '../app/scheduled-tasks/types' +} from '../../app/scheduled-tasks/types' dayjs.extend(relativeTime) diff --git a/packages/browseros-agent/apps/agent/lib/constants/analyticsEvents.ts b/packages/browseros-agent/apps/agent/lib/constants/analyticsEvents.ts index 3036950a1..e015aa4b6 100644 --- a/packages/browseros-agent/apps/agent/lib/constants/analyticsEvents.ts +++ b/packages/browseros-agent/apps/agent/lib/constants/analyticsEvents.ts @@ -216,6 +216,10 @@ export const SCHEDULED_TASK_CANCELLED_EVENT = /** @public */ export const SCHEDULED_TASK_RETRIED_EVENT = 'settings.scheduled_task.retried' +/** @public */ +export const SCHEDULED_TASK_CONTINUE_IN_CHAT_EVENT = + 'settings.scheduled_task.continue_in_chat' + /** @public */ export const JTBD_POPUP_DISMISSED_EVENT = 'ui.jtbd_popup.dismissed' From ef1f95421399b32ecd416dbd77d1faee32729b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CENK=20TEK=C4=B0N?= <143123890+cenktekin@users.noreply.github.com> Date: Fri, 8 May 2026 12:49:14 +0300 Subject: [PATCH 4/5] fix(agent): use sessionStorage for scheduled task result transfer Address Copilot review: avoid passing full task output via URL query params which can exceed browser limits and expose sensitive data. - Store result in sessionStorage with run-specific key - Pass key reference via URL (not the full result) - NewTabChat reads from sessionStorage when source=scheduled-task - Clean up sessionStorage after reading Co-authored-by: Copilot --- .../components/ai-elements/run-result-dialog.tsx | 6 +++++- .../agent/entrypoints/newtab/index/NewTabChat.tsx | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/browseros-agent/apps/agent/components/ai-elements/run-result-dialog.tsx b/packages/browseros-agent/apps/agent/components/ai-elements/run-result-dialog.tsx index 1e9d2f98a..fa3ab5843 100644 --- a/packages/browseros-agent/apps/agent/components/ai-elements/run-result-dialog.tsx +++ b/packages/browseros-agent/apps/agent/components/ai-elements/run-result-dialog.tsx @@ -72,7 +72,11 @@ export const RunResultDialog: FC = ({ if (!run?.result) return onOpenChange(false) track(SCHEDULED_TASK_CONTINUE_IN_CHAT_EVENT, { mode }) - navigate(`/home/chat?q=${encodeURIComponent(run.result)}&mode=${mode}`) + const key = `scheduled-task-${run.id}` + sessionStorage.setItem(key, run.result) + navigate( + `/home/chat?q=${encodeURIComponent(key)}&mode=${mode}&source=scheduled-task`, + ) } const handleCopy = async () => { diff --git a/packages/browseros-agent/apps/agent/entrypoints/newtab/index/NewTabChat.tsx b/packages/browseros-agent/apps/agent/entrypoints/newtab/index/NewTabChat.tsx index 164c3afc4..7d72a6633 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/newtab/index/NewTabChat.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/newtab/index/NewTabChat.tsx @@ -79,10 +79,11 @@ export const NewTabChat: FC = () => { // biome-ignore lint/correctness/useExhaustiveDependencies: must only run once on mount useEffect(() => { if (hasSentInitialRef.current) return - const query = searchParams.get('q') + const queryParam = searchParams.get('q') const chatMode = searchParams.get('mode') const tabIdsParam = searchParams.get('tabs') - if (!query) return + const source = searchParams.get('source') + if (!queryParam) return hasSentInitialRef.current = true if (chatMode === 'chat' || chatMode === 'agent') { @@ -90,6 +91,15 @@ export const NewTabChat: FC = () => { } setSearchParams({}, { replace: true }) + const query = + source === 'scheduled-task' + ? (sessionStorage.getItem(queryParam) ?? queryParam) + : queryParam + + if (source === 'scheduled-task') { + sessionStorage.removeItem(queryParam) + } + const actionType = searchParams.get('actionType') const tabName = searchParams.get('tabName') const tabDescription = searchParams.get('tabDescription') From 6d3b8669d225973658ef7943945727e27a69127a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CENK=20TEK=C4=B0N?= <143123890+cenktekin@users.noreply.github.com> Date: Fri, 8 May 2026 12:51:43 +0300 Subject: [PATCH 5/5] fix(agent): address Copilot review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing ScheduledJobRun import in ScheduledTaskResultGroup.tsx - Optimize groupRunsByJob: O(n²) → O(n) by precomputing jobs Map Co-authored-by: Copilot --- .../app/scheduled-tasks/ScheduledTaskResultGroup.tsx | 2 +- .../apps/agent/entrypoints/app/scheduled-tasks/types.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx index b32667483..295a9a03a 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/ScheduledTaskResultGroup.tsx @@ -16,7 +16,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import type { JobRunWithDetails, ScheduledJob } from './types' +import type { JobRunWithDetails, ScheduledJob, ScheduledJobRun } from './types' dayjs.extend(relativeTime) diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/types.ts b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/types.ts index 21e2ad516..f4d9c9067 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/types.ts +++ b/packages/browseros-agent/apps/agent/entrypoints/app/scheduled-tasks/types.ts @@ -25,10 +25,11 @@ export function groupRunsByJob( jobRuns: ScheduledJobRun[], jobs: ScheduledJob[], ): JobGroup[] { + const jobsById = new Map(jobs.map((j) => [j.id, j])) const runsByJob = new Map() for (const run of jobRuns) { - const job = jobs.find((j) => j.id === run.jobId) ?? { + const job = jobsById.get(run.jobId) ?? { id: run.jobId, name: 'Unknown task', query: '',