Skip to content

Commit 8524373

Browse files
committed
fix (tasks): minor bug fixes
1 parent 9175198 commit 8524373

File tree

12 files changed

+261
-89
lines changed

12 files changed

+261
-89
lines changed

src/client/app/tasks/page.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ function TasksPageContent() {
105105
const [groupBy, setGroupBy] = useState("status")
106106
const [isAddingNewTask, setIsAddingNewTask] = useState(false)
107107

108+
// Keep selectedTask in sync with the main tasks list
109+
useEffect(() => {
110+
if (selectedTask) {
111+
const updatedSelectedTask = tasks.find(
112+
(t) => t.task_id === selectedTask.task_id
113+
)
114+
if (updatedSelectedTask) {
115+
setSelectedTask(updatedSelectedTask)
116+
} else {
117+
// If task is not in the list anymore (e.g., deleted), close the panel.
118+
setSelectedTask(null)
119+
}
120+
}
121+
}, [tasks, selectedTask])
122+
108123
// Check for query param to auto-open demo on first visit from onboarding
109124
useEffect(() => {
110125
const showDemo = searchParams.get("show_demo")
@@ -167,6 +182,17 @@ function TasksPageContent() {
167182
// eslint-disable-next-line react-hooks/exhaustive-deps
168183
}, [])
169184

185+
// Listen for custom event from LayoutWrapper to refresh tasks
186+
useEffect(() => {
187+
const handleBackendUpdate = () => {
188+
console.log("Received tasksUpdatedFromBackend event, fetching tasks...")
189+
toast.success("Task list updated from backend.")
190+
fetchTasks()
191+
}
192+
window.addEventListener("tasksUpdatedFromBackend", handleBackendUpdate)
193+
return () => window.removeEventListener("tasksUpdatedFromBackend", handleBackendUpdate)
194+
}, [fetchTasks])
195+
170196
// Listen for real-time progress updates from WebSocket
171197
useEffect(() => {
172198
const handleProgressUpdate = (event) => {
@@ -349,6 +375,17 @@ function TasksPageContent() {
349375
)
350376
}
351377

378+
const handleSendTaskChatMessage = (taskId, message) =>
379+
handleAction(
380+
() =>
381+
fetch("/api/tasks/chat", {
382+
method: "POST",
383+
headers: { "Content-Type": "application/json" },
384+
body: JSON.stringify({ taskId, message })
385+
}),
386+
"Message sent. Re-planning task..."
387+
)
388+
352389
const handleUpdateTask = async (updatedTask) => {
353390
await handleAction(
354391
() =>
@@ -450,18 +487,13 @@ function TasksPageContent() {
450487
handleRerunTask(taskId)
451488
setSelectedTask(null)
452489
}}
453-
onAnswerClarifications={(taskId, answers) => {
454-
handleAnswerClarifications(taskId, answers)
455-
setSelectedTask(null)
456-
}}
490+
onAnswerClarifications={handleAnswerClarifications}
457491
onArchiveTask={(taskId) => {
458492
handleArchiveTask(taskId)
459493
setSelectedTask(null)
460494
}}
461-
onMarkComplete={(taskId) => {
462-
handleMarkComplete(taskId)
463-
setSelectedTask(null)
464-
}}
495+
onMarkComplete={handleMarkComplete}
496+
onSendChatMessage={handleSendTaskChatMessage}
465497
/>
466498
</div>
467499

src/client/components/LayoutWrapper.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ export default function LayoutWrapper({ children }) {
9191
detail: data.payload
9292
})
9393
)
94+
} else if (data.type === "task_list_updated") {
95+
// This is a generic event telling the app that tasks have changed
96+
// on the backend and the UI should refetch them.
97+
window.dispatchEvent(
98+
new CustomEvent("tasksUpdatedFromBackend")
99+
)
94100
}
95101
}
96102
ws.onclose = () => {

src/client/components/NotificationsOverlay.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import {
1111
IconX,
1212
IconArrowRight
1313
} from "@tabler/icons-react"
14-
import { formatDistanceToNow } from "date-fns"
14+
import { formatDistanceToNow, parseISO } from "date-fns"
1515

1616
const NotificationItem = ({ notification, onDelete, onClick }) => {
1717
const formattedTimestamp = formatDistanceToNow(
18-
new Date(notification.timestamp),
18+
parseISO(notification.timestamp),
1919
{ addSuffix: true }
2020
)
2121

src/client/components/PostHogProvider.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ export function PostHogProvider({ children }) {
1212
ui_host:
1313
process.env.NEXT_PUBLIC_POSTHOG_HOST ||
1414
"https://us.posthog.com",
15-
defaults: "2025-05-24",
1615
capture_exceptions: true,
17-
debug: process.env.NODE_ENV === "development"
16+
autocapture: false // Disable autocapture to reduce event volume
1817
})
1918
}
2019
}, [])

src/client/components/tasks/ScheduleEditor.js

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@ const ScheduleEditor = ({ schedule, setSchedule }) => {
1717
setSchedule(baseSchedule)
1818
}
1919

20+
const handleRunAtChange = (e) => {
21+
const localDateTimeString = e.target.value
22+
if (localDateTimeString) {
23+
// The input value is a string like "2024-07-26T09:00".
24+
// new Date() will parse this as 9 AM in the browser's local timezone.
25+
const localDate = new Date(localDateTimeString)
26+
// .toISOString() converts it to a UTC string, which is what we want to store.
27+
setSchedule({ ...schedule, run_at: localDate.toISOString() })
28+
} else {
29+
setSchedule({ ...schedule, run_at: null })
30+
}
31+
}
32+
33+
const getLocalDateTimeString = (isoString) => {
34+
if (!isoString) return ""
35+
const date = new Date(isoString)
36+
const tzOffset = date.getTimezoneOffset() * 60000 // offset in milliseconds
37+
const localISOTime = new Date(date.getTime() - tzOffset)
38+
.toISOString()
39+
.slice(0, 16)
40+
return localISOTime
41+
}
42+
2043
const handleDayToggle = (day) => {
2144
const currentDays = schedule.days || []
2245
const newDays = currentDays.includes(day)
@@ -55,16 +78,8 @@ const ScheduleEditor = ({ schedule, setSchedule }) => {
5578
</label>
5679
<input
5780
type="datetime-local"
58-
value={
59-
schedule.run_at
60-
? new Date(schedule.run_at)
61-
.toISOString()
62-
.slice(0, 16)
63-
: ""
64-
}
65-
onChange={(e) =>
66-
setSchedule({ ...schedule, run_at: e.target.value })
67-
}
81+
value={getLocalDateTimeString(schedule.run_at)}
82+
onChange={handleRunAtChange}
6883
className="w-full p-2 bg-neutral-700 border border-neutral-600 rounded-md focus:border-blue-500"
6984
/>
7085
<p className="text-xs text-gray-500 mt-1">
@@ -94,18 +109,9 @@ const ScheduleEditor = ({ schedule, setSchedule }) => {
94109
</select>
95110
</div>
96111
<div>
97-
<label
98-
className="text-xs text-gray-400 block mb-1"
99-
data-tooltip-id="schedule-tooltip"
100-
data-tooltip-content="Tasks are scheduled in Coordinated Universal Time (UTC) to ensure consistency across timezones."
101-
>
102-
Time (UTC)
112+
<label className="text-xs text-gray-400 block mb-1">
113+
Time (Local)
103114
</label>
104-
<Tooltip
105-
place="top"
106-
id="schedule-tooltip"
107-
style={{ zIndex: 99999 }}
108-
/>
109115
<input
110116
type="time"
111117
value={schedule.time || "09:00"}

src/client/components/tasks/TaskDetailsContent.js

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import React, { useState } from "react"
3+
import React, { useState, useEffect, useRef } from "react"
44
import toast from "react-hot-toast"
55
import { cn } from "@utils/cn"
66
import { taskStatusColors, priorityMap } from "./constants"
@@ -10,10 +10,59 @@ import {
1010
IconSparkles,
1111
IconUser,
1212
IconX,
13-
IconLoader
13+
IconLoader,
14+
IconSend
1415
} from "@tabler/icons-react"
1516
import ScheduleEditor from "@components/tasks/ScheduleEditor"
1617
import ExecutionUpdate from "./ExecutionUpdate"
18+
import ChatBubble from "@components/ChatBubble"
19+
20+
const TaskChatSection = ({ task, onSendChatMessage }) => {
21+
const [message, setMessage] = useState("")
22+
const chatEndRef = React.useRef(null)
23+
24+
useEffect(() => {
25+
chatEndRef.current?.scrollIntoView({ behavior: "smooth" })
26+
}, [task.chat_history])
27+
28+
const handleSend = () => {
29+
if (message.trim()) {
30+
onSendChatMessage(task.task_id, message)
31+
setMessage("")
32+
}
33+
}
34+
35+
return (
36+
<div className="mt-6 pt-6 border-t border-neutral-800">
37+
<h4 className="font-semibold text-neutral-300 mb-4">
38+
Task Conversation
39+
</h4>
40+
<div className="space-y-4 max-h-64 overflow-y-auto custom-scrollbar pr-2">
41+
{(task.chat_history || []).map((msg, index) => (
42+
<ChatBubble key={index} message={msg.content} isUser={msg.role === 'user'} />
43+
))}
44+
<div ref={chatEndRef} />
45+
</div>
46+
<div className="mt-4 flex items-center gap-2">
47+
<input
48+
type="text"
49+
value={message}
50+
onChange={(e) => setMessage(e.target.value)}
51+
onKeyDown={(e) => e.key === "Enter" && handleSend()}
52+
placeholder="Ask for changes or follow-ups..."
53+
className="flex-grow p-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm"
54+
/>
55+
<button
56+
onClick={handleSend}
57+
className="p-2 bg-blue-600 rounded-lg text-white hover:bg-blue-500 disabled:opacity-50"
58+
disabled={!message.trim()}
59+
>
60+
<IconSend size={16} />
61+
</button>
62+
</div>
63+
</div>
64+
)
65+
}
1766

1867
// New component for handling clarification questions
1968
const ClarificationInputSection = ({ run, task, onAnswerClarifications }) => {
@@ -98,7 +147,8 @@ const TaskDetailsContent = ({
98147
handleStepChange,
99148
allTools,
100149
integrations,
101-
onAnswerClarifications
150+
onAnswerClarifications,
151+
onSendChatMessage
102152
}) => {
103153
if (!task) {
104154
return null
@@ -432,6 +482,10 @@ const TaskDetailsContent = ({
432482
</div>
433483
))
434484
)}
485+
486+
{(task.status === "completed" || (task.chat_history && task.chat_history.length > 0)) && (
487+
<TaskChatSection task={task} onSendChatMessage={onSendChatMessage} />
488+
)}
435489
</div>
436490
)
437491
}

src/client/components/tasks/TaskDetailsPanel.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ const TaskDetailsPanel = ({
2828
onAnswerClarifications,
2929
onArchiveTask,
3030
onMarkComplete,
31-
className
31+
className,
32+
onSendChatMessage
3233
}) => {
3334
const [isEditing, setIsEditing] = useState(false)
3435
const [editableTask, setEditableTask] = useState(task)
@@ -142,6 +143,7 @@ const TaskDetailsPanel = ({
142143
allTools={allTools}
143144
integrations={integrations}
144145
onAnswerClarifications={onAnswerClarifications}
146+
onSendChatMessage={onSendChatMessage}
145147
/>
146148
</main>
147149

src/server/main/tasks/prompts.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
- `0`: High priority (urgent, important, deadlines).
1313
- `1`: Medium priority (standard tasks, default).
1414
- `2`: Low priority (can be done anytime, not urgent).
15-
3. **Schedule:** Analyze the prompt for any scheduling information (dates, times, recurrence).
16-
- If no specific time is mentioned, set the schedule to `null`.
17-
- If a specific date and time is mentioned for a one-time task, use the `once` type. The `run_at` value MUST be in `YYYY-MM-DDTHH:MM` format for local datetime inputs.
18-
- If the task is recurring, use the `recurring` type.
15+
3. **Schedule:** Analyze the prompt for any scheduling information (dates, times, recurrence). Decipher whether the task is a one-time event or recurring, and format the schedule accordingly:
16+
- If no specific time is mentioned, the task has to be performed right now.
17+
- If a specific date and time is mentioned and its a one-time task, use the `once` type. The `run_at` value MUST be in `YYYY-MM-DDTHH:MM` format for local datetime inputs.
18+
- If the task is found to be recurring, use the `recurring` type.
1919
- `frequency` can be "daily" or "weekly".
2020
- `time` MUST be in "HH:MM" 24-hour format.
2121
- For "weekly" frequency, `days` MUST be a list of full day names (e.g., ["Monday", "Wednesday"]).
@@ -61,7 +61,10 @@
6161
{{
6262
"description": "Organize my downloads folder",
6363
"priority": 2,
64-
"schedule": null
64+
"schedule": {{
65+
"type": "once",
66+
"run_at": "CURRENT_DATE_TIME_IN_USER_TIMEZONE"
67+
}}
6568
}}
6669
```
67-
"""
70+
"""

src/server/main/tasks/routes.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ async def update_task(
139139
update_data = request.dict(exclude_unset=True)
140140
update_data.pop("taskId", None)
141141

142+
# If the schedule is being updated, inject the user's timezone for recurring tasks.
143+
if 'schedule' in update_data and update_data['schedule']:
144+
if update_data['schedule'].get('type') == 'recurring':
145+
user_profile = await mongo_manager.get_user_profile(user_id)
146+
user_timezone_str = user_profile.get("userData", {}).get("personalInfo", {}).get("timezone", "UTC")
147+
update_data['schedule']['timezone'] = user_timezone_str
148+
142149
# If assignee is changed to 'ai' and task is in a non-planned state, trigger planning
143150
if 'assignee' in update_data and update_data['assignee'] == 'ai':
144151
if task.get('status') == 'pending': # Assuming 'pending' is the status for user-assigned tasks
@@ -246,6 +253,7 @@ async def approve_task(
246253
schedule_data = task_doc.get("schedule") or {}
247254

248255
if schedule_data.get("type") == "recurring":
256+
# The schedule object should contain the user's timezone, added when the task was created/updated.
249257
next_run = calculate_next_run(schedule_data)
250258
if next_run:
251259
update_data["next_execution_at"] = next_run
@@ -256,7 +264,11 @@ async def approve_task(
256264
update_data["error"] = "Could not calculate next run time for recurring task."
257265
elif schedule_data.get("type") == "once" and schedule_data.get("run_at"):
258266
run_at_time_str = schedule_data.get("run_at")
259-
run_at_time = datetime.fromisoformat(run_at_time_str).replace(tzinfo=timezone.utc)
267+
run_at_time = datetime.fromisoformat(run_at_time_str)
268+
# Ensure datetime is timezone-aware for comparison. New data will have 'Z' and be aware.
269+
if run_at_time.tzinfo is None:
270+
# This branch is for backward compatibility. It ASSUMES the naive datetime was stored as UTC.
271+
run_at_time = run_at_time.replace(tzinfo=timezone.utc)
260272
if run_at_time > datetime.now(timezone.utc):
261273
update_data["next_execution_at"] = run_at_time
262274
update_data["status"] = "pending"
@@ -348,4 +360,22 @@ async def internal_progress_update(request: ProgressUpdateRequest):
348360
except Exception as e:
349361
logger.error(f"Failed to push progress update via websocket for task {request.task_id}: {e}", exc_info=True)
350362
# Don't fail the worker, just log it.
351-
return {"status": "error", "detail": str(e)}
363+
return {"status": "error", "detail": str(e)}
364+
365+
@router.post("/internal/task-update-push", include_in_schema=False)
366+
async def internal_task_update_push(request: ProgressUpdateRequest): # Reusing model for convenience
367+
"""
368+
Internal endpoint for workers to tell the main server to push a generic
369+
'tasks have changed' notification to the client via WebSocket.
370+
"""
371+
logger.info(f"Received internal request to push task list update for user {request.user_id}")
372+
try:
373+
await websocket_manager.send_personal_json_message(
374+
{"type": "task_list_updated"},
375+
request.user_id,
376+
connection_type="notifications"
377+
)
378+
return {"status": "success"}
379+
except Exception as e:
380+
logger.error(f"Failed to push task list update via websocket for user {request.user_id}: {e}", exc_info=True)
381+
return {"status": "error", "detail": str(e)}

0 commit comments

Comments
 (0)