Skip to content

feat: implemented background requests #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ce95bad
feat: implement background job status handling with API integration
nickytonline Jul 23, 2025
91f44b3
feat: add BackgroundJobItem component with job status handling and ac…
nickytonline Jul 23, 2025
7978975
feat: implement useBackgroundJobs hook for job management with local …
nickytonline Jul 23, 2025
56e7c49
feat: add BackgroundJobsSidebar and Sidebar components for background…
nickytonline Jul 23, 2025
3893537
feat: add BackgroundToggle component with state management and toolti…
nickytonline Jul 23, 2025
28b36e5
feat: enhance useStreamingChat with background job handling and respo…
nickytonline Jul 23, 2025
e2bd45d
fix: add missing key prop to list items in BackgroundJobsSidebar comp…
nickytonline Jul 23, 2025
00fc120
feat: add background handling to chat request processing
nickytonline Jul 23, 2025
ed814d8
feat: add GET endpoint for retrieving background job as a streamed re…
nickytonline Jul 23, 2025
6b89d37
feat: integrate background job handling in Chat component with UI upd…
nickytonline Jul 23, 2025
8dfa5de
feat: fixed polling in useBackgroundJobStatus hook
nickytonline Jul 23, 2025
8d9bbc6
chore: formatting and lint fixes
nickytonline Jul 23, 2025
143bfaf
fix: fixed broken test after over eslinting
nickytonline Jul 23, 2025
793ed39
refactor: remove debug logging from job status update effect
nickytonline Jul 23, 2025
2409269
feat: add zustand as a dependency
nickytonline Jul 23, 2025
5d7686a
feat: implement zustand store for background jobs management
nickytonline Jul 23, 2025
16956c4
test: update job IDs in tests to follow new format in Zod schema
nickytonline Jul 23, 2025
091a4eb
fix: cancel active streams and clear messages on background job start
nickytonline Jul 23, 2025
e9599d4
chore: formatting fixes
nickytonline Jul 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { Preview } from '@storybook/react-vite'
import '../src/styles.css'

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
})

const preview: Preview = {
parameters: {
darkMode: {
Expand All @@ -21,9 +31,11 @@ const preview: Preview = {
},
decorators: [
(Story) => (
<div className="p-4">
<Story />
</div>
<QueryClientProvider client={queryClient}>
<div className="p-4">
<Story />
</div>
</QueryClientProvider>
),
],
}
Expand Down
32 changes: 31 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.0",
"vite-tsconfig-paths": "^5.1.4",
"zod": "^3.24.2"
"zod": "^3.24.2",
"zustand": "^5.0.6"
},
"devDependencies": {
"@chromatic-com/storybook": "^4.0.1",
Expand Down
85 changes: 85 additions & 0 deletions src/components/BackgroundJobItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { BackgroundJobItem } from './BackgroundJobItem'
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { BackgroundJob } from '@/lib/schemas'

const meta: Meta<typeof BackgroundJobItem> = {
title: 'Components/BackgroundJobItem',
component: BackgroundJobItem,
parameters: {
layout: 'padded',
},
argTypes: {
onLoadResponse: { action: 'loadResponse' },
onCancelJob: { action: 'cancelJob' },
updateJob: { action: 'updateJob' },
removeJob: { action: 'removeJob' },
},
}

export default meta
type Story = StoryObj<typeof BackgroundJobItem>

const baseJob: BackgroundJob = {
id: 'job-1',
status: 'running',
createdAt: '2024-01-15T10:30:00Z',
title: 'Analyzing code repository structure',
}

export const Running: Story = {
args: {
job: baseJob,
},
}

export const Completed: Story = {
args: {
job: {
...baseJob,
status: 'completed',
completedAt: '2024-01-15T10:35:00Z',
response:
'Analysis complete. Found 15 components, 8 hooks, and 12 utility functions.',
},
},
}

export const Failed: Story = {
args: {
job: {
...baseJob,
status: 'failed',
completedAt: '2024-01-15T10:32:00Z',
},
},
}

export const FailedWithResponse: Story = {
args: {
job: {
...baseJob,
status: 'failed',
completedAt: '2024-01-15T10:32:00Z',
response: 'Partial analysis completed before error occurred.',
error: 'Network timeout during file analysis.',
},
},
}

export const LongTitle: Story = {
args: {
job: {
...baseJob,
title:
'Performing comprehensive security audit of the entire application including all dependencies and configuration files',
},
},
}

export const NoCallbacks: Story = {
args: {
job: baseJob,
onLoadResponse: undefined,
onCancelJob: undefined,
},
}
182 changes: 182 additions & 0 deletions src/components/BackgroundJobItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { Clock, Play, Trash2, X } from 'lucide-react'
import { useEffect } from 'react'
import { Button } from './ui/button'
import type { BackgroundJob } from '@/lib/schemas'
import { useBackgroundJobStatus } from '@/hooks/useBackgroundJobStatus'

export function BackgroundJobItem({
job,
onLoadResponse,
onCancelJob,
updateJob,
removeJob,
}: {
job: BackgroundJob
onLoadResponse?: (jobId: string, response: string) => void
onCancelJob?: (jobId: string) => void
updateJob: (jobId: string, updates: Partial<BackgroundJob>) => void
removeJob: (jobId: string) => void
}) {
// Use polling hook to check job status - continue polling until definitely complete or failed
const shouldPoll =
job.status === 'running' || (job.status === 'failed' && !job.completedAt)

const { data: statusData } = useBackgroundJobStatus(
shouldPoll ? job : undefined,
2000,
)

// Update job when status changes
useEffect(() => {
if (statusData) {
const needsUpdate =
statusData.status !== job.status ||
statusData.response !== job.response ||
statusData.error !== job.error ||
statusData.completedAt !== job.completedAt

if (needsUpdate) {
updateJob(job.id, {
status: statusData.status,
response: statusData.response || job.response,
error: statusData.error,
completedAt: statusData.completedAt || job.completedAt,
})
}
}
}, [statusData, job, updateJob])

const handleLoadResponse = () => {
if (onLoadResponse) {
// If no response yet, load a placeholder or empty content
const responseContent =
job.response ||
'[Job is still running - partial response will appear here]'
onLoadResponse(job.id, responseContent)
}
}

const handleCancelJob = () => {
if (onCancelJob) {
onCancelJob(job.id)
}
// Update job status to indicate cancellation attempt
updateJob(job.id, { status: 'failed', error: 'Cancelled by user' })
}

const handleDeleteJob = () => {
removeJob(job.id)
}

const getStatusIcon = (status: BackgroundJob['status']) => {
switch (status) {
case 'running':
return <Clock className="h-4 w-4 text-blue-500 animate-spin" />
case 'completed':
return <Play className="h-4 w-4 text-green-500" />
case 'failed':
return <X className="h-4 w-4 text-red-500" />
default:
return <Clock className="h-4 w-4 text-gray-500" />
}
}

const getStatusText = (status: BackgroundJob['status']) => {
switch (status) {
case 'running':
return 'Running'
case 'completed':
return 'Completed'
case 'failed':
return 'Failed'
default:
return 'Unknown'
}
}

const formatTimestamp = (timestamp: string) => {
try {
return new Date(timestamp).toLocaleString()
} catch {
return timestamp
}
}

return (
<section
className="grid gap-2 border rounded-lg p-4 space-y-3 bg-card"
aria-label={job.title}
>
<header className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getStatusIcon(job.status)}
<span className="font-medium">{getStatusText(job.status)}</span>
</div>
<time
className="text-xs text-muted-foreground"
dateTime={job.completedAt ?? job.createdAt}
>
{formatTimestamp(job.createdAt)}
</time>
</header>

<span className="text-sm font-medium truncate" title={job.title}>
{job.title}
</span>

{job.error && (
<div
className="text-sm text-red-600 bg-red-50 p-2 rounded"
role="alert"
aria-live="assertive"
>
{job.error}
</div>
)}

<nav
aria-label={`Job actions for background job ${job.title}`}
className="flex justify-end"
>
<ul className="flex gap-1">
<li>
<Button
variant="outline"
size="sm"
onClick={handleLoadResponse}
disabled={job.status === 'failed' && !job.response}
>
<Play className="h-3 w-3 mr-1" />
<span className="sr-only md:not-sr-only">
{job.status === 'running' && !job.response
? 'Load Progress'
: job.status === 'running' && job.response
? 'Load Partial'
: 'Load Response'}
</span>
</Button>
</li>
{job.status === 'running' && (
<li>
<Button variant="outline" size="sm" onClick={handleCancelJob}>
<X className="h-3 w-3 mr-1" />
<span>Cancel</span>
</Button>
</li>
)}
<li>
<Button
variant="outline"
size="sm"
onClick={handleDeleteJob}
className="px-2"
>
<Trash2 className="h-3 w-3" />
<span className="sr-only md:not-sr-only">Delete</span>
</Button>
</li>
</ul>
</nav>
</section>
)
}
Loading
Loading