Squashed commit of the following:

commit ec3438a00b193510217da82ddf15a2c44caaa611 Author: Rafael Uzarowski <uzarowski.rafael@proton.me> Date: Thu Nov 13 00:30:22 2025 +0100 fix: task chats payload fix in poll() commit b7f9afdb1dece31f64862437f74e8a2a5551f237 Author: Rafael Uzarowski <uzarowski.rafael@proton.me> Date: Thu Nov 13 00:20:24 2025 +0100 feat: Project Scheduler Tasks

frdel committed Nov 13, 2025 at 09:00 UTC e9b368df1526297a95d0c049b2da31b70bea1df3
10 files changed +381 -66
python/api/poll.py
+7 -1
@@ -88,6 +88,12 @@ class Poll(ApiHandler):
88 "last_result": task_details.get("last_result"),
89 "attachments": task_details.get("attachments", []),
90 "context_id": task_details.get("context_id"),
91 + "project_name": task_details.get("project_name"),
92 + "project_color": task_details.get("project_color"),
93 + "project": {
94 + "name": task_details.get("project_name"),
95 + "color": task_details.get("project_color"),
96 + },
97 })
98
99 # Add type-specific fields
@@ -122,4 +128,4 @@ class Poll(ApiHandler):
128 "notifications": notifications,
129 "notifications_guid": notification_manager.guid,
130 "notifications_version": len(notification_manager.updates),
125 - }
\ No newline at end of file
131 + }
python/api/scheduler_task_create.py
+32 -5
@@ -3,6 +3,7 @@ from python.helpers.task_scheduler import (
3 TaskScheduler, ScheduledTask, AdHocTask, PlannedTask, TaskSchedule,
4 serialize_task, parse_task_schedule, parse_task_plan, TaskType
5 )
6 +from python.helpers.projects import load_basic_project_data
7 from python.helpers.localization import Localization
8 from python.helpers.print_style import PrintStyle
9 import random
@@ -27,7 +28,26 @@ class SchedulerTaskCreate(ApiHandler):
28 system_prompt = input.get("system_prompt", "")
29 prompt = input.get("prompt")
30 attachments = input.get("attachments", [])
30 - context_id = input.get("context_id", None)
31 +
32 + requested_project_slug = input.get("project_name")
33 + if isinstance(requested_project_slug, str):
34 + requested_project_slug = requested_project_slug.strip() or None
35 + else:
36 + requested_project_slug = None
37 +
38 + project_slug = requested_project_slug
39 + project_color = None
40 +
41 + if project_slug:
42 + try:
43 + metadata = load_basic_project_data(requested_project_slug)
44 + project_color = metadata.get("color") or None
45 + except Exception as exc:
46 + printer.error(f"SchedulerTaskCreate: failed to load project '{project_slug}': {exc}")
47 + return {"error": f"Saving project failed: {project_slug}"}
48 +
49 + # Always dedicated context for scheduler tasks created by ui
50 + task_context_id = None
51
52 # Check if schedule is provided (for ScheduledTask)
53 schedule = input.get("schedule", {})
@@ -77,8 +97,10 @@ class SchedulerTaskCreate(ApiHandler):
97 prompt=prompt,
98 schedule=task_schedule,
99 attachments=attachments,
80 - context_id=context_id,
81 - timezone=timezone
100 + context_id=task_context_id,
101 + timezone=timezone,
102 + project_name=project_slug,
103 + project_color=project_color,
104 )
105 elif plan:
106 # Create a planned task
@@ -94,7 +116,9 @@ class SchedulerTaskCreate(ApiHandler):
116 prompt=prompt,
117 plan=task_plan,
118 attachments=attachments,
97 - context_id=context_id
119 + context_id=task_context_id,
120 + project_name=project_slug,
121 + project_color=project_color,
122 )
123 else:
124 # Create an ad-hoc task
@@ -105,7 +129,9 @@ class SchedulerTaskCreate(ApiHandler):
129 prompt=prompt,
130 token=token,
131 attachments=attachments,
108 - context_id=context_id
132 + context_id=task_context_id,
133 + project_name=project_slug,
134 + project_color=project_color,
135 )
136 # Verify token after creation
137 if isinstance(task, AdHocTask):
@@ -132,5 +158,6 @@ class SchedulerTaskCreate(ApiHandler):
158 printer.print(f"Serialized adhoc task, token in response: '{task_dict.get('token')}'")
159
160 return {
161 + "ok": True,
162 "task": task_dict
163 }
python/api/scheduler_task_update.py
+4
@@ -48,6 +48,9 @@ class SchedulerTaskUpdate(ApiHandler):
48 if "attachments" in input:
49 update_params["attachments"] = input.get("attachments", [])
50
51 + if "project_name" in input or "project_color" in input:
52 + return {"error": "Project changes are not allowed"}
53 +
54 # Update schedule if this is a scheduled task and schedule is provided
55 if isinstance(task, ScheduledTask) and "schedule" in input:
56 schedule_data = input.get("schedule", {})
@@ -85,5 +88,6 @@ class SchedulerTaskUpdate(ApiHandler):
88 task_dict = serialize_task(updated_task)
89
90 return {
91 + "ok": True,
92 "task": task_dict
93 }
python/api/scheduler_tasks_list.py
+2 -2
@@ -22,8 +22,8 @@ class SchedulerTasksList(ApiHandler):
22 # Use the scheduler's convenience method for task serialization
23 tasks_list = scheduler.serialize_all_tasks()
24
25 - return {"tasks": tasks_list}
25 + return {"ok": True, "tasks": tasks_list}
26
27 except Exception as e:
28 PrintStyle.error(f"Failed to list tasks: {str(e)} {traceback.format_exc()}")
29 - return {"error": f"Failed to list tasks: {str(e)} {traceback.format_exc()}", "tasks": []}
29 + return {"ok": False, "error": f"Failed to list tasks: {str(e)} {traceback.format_exc()}", "tasks": []}
python/helpers/task_scheduler.py
+34 -8
@@ -124,6 +124,8 @@ class BaseTask(BaseModel):
124 system_prompt: str
125 prompt: str
126 attachments: list[str] = Field(default_factory=list)
127 + project_name: str | None = Field(default=None)
128 + project_color: str | None = Field(default=None)
129 created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
130 updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
131 last_run: datetime | None = None
@@ -181,6 +183,9 @@ class BaseTask(BaseModel):
183 def get_next_run(self) -> datetime | None:
184 return None
185
186 + def is_dedicated(self) -> bool:
187 + return self.context_id == self.uuid
188 +
189 def get_next_run_minutes(self) -> int | None:
190 next_run = self.get_next_run()
191 if next_run is None:
@@ -243,14 +248,18 @@ class AdHocTask(BaseTask):
248 prompt: str,
249 token: str,
250 attachments: list[str] = list(),
246 - context_id: str | None = None
251 + context_id: str | None = None,
252 + project_name: str | None = None,
253 + project_color: str | None = None
254 ):
255 return cls(name=name,
256 system_prompt=system_prompt,
257 prompt=prompt,
258 attachments=attachments,
259 token=token,
253 - context_id=context_id)
260 + context_id=context_id,
261 + project_name=project_name,
262 + project_color=project_color)
263
264 def update(self,
265 name: str | None = None,
@@ -288,7 +297,9 @@ class ScheduledTask(BaseTask):
297 schedule: TaskSchedule,
298 attachments: list[str] = list(),
299 context_id: str | None = None,
291 - timezone: str | None = None
300 + timezone: str | None = None,
301 + project_name: str | None = None,
302 + project_color: str | None = None,
303 ):
304 # Set timezone in schedule if provided
305 if timezone is not None:
@@ -301,7 +312,9 @@ class ScheduledTask(BaseTask):
312 prompt=prompt,
313 attachments=attachments,
314 schedule=schedule,
304 - context_id=context_id)
315 + context_id=context_id,
316 + project_name=project_name,
317 + project_color=project_color)
318
319 def update(self,
320 name: str | None = None,
@@ -365,14 +378,18 @@ class PlannedTask(BaseTask):
378 prompt: str,
379 plan: TaskPlan,
380 attachments: list[str] = list(),
368 - context_id: str | None = None
381 + context_id: str | None = None,
382 + project_name: str | None = None,
383 + project_color: str | None = None
384 ):
385 return cls(name=name,
386 system_prompt=system_prompt,
387 prompt=prompt,
388 plan=plan,
389 attachments=attachments,
375 - context_id=context_id)
390 + context_id=context_id,
391 + project_name=project_name,
392 + project_color=project_color)
393
394 def update(self,
395 name: str | None = None,
@@ -1037,12 +1054,19 @@ def serialize_task(task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> Dict[s
1054 "system_prompt": task.system_prompt,
1055 "prompt": task.prompt,
1056 "attachments": task.attachments,
1057 + "project_name": task.project_name,
1058 + "project_color": task.project_color,
1059 "created_at": serialize_datetime(task.created_at),
1060 "updated_at": serialize_datetime(task.updated_at),
1061 "last_run": serialize_datetime(task.last_run),
1062 "next_run": serialize_datetime(task.get_next_run()),
1063 "last_result": task.last_result,
1045 - "context_id": task.context_id
1064 + "context_id": task.context_id,
1065 + "dedicated_context": task.is_dedicated(),
1066 + "project": {
1067 + "name": task.project_name,
1068 + "color": task.project_color,
1069 + },
1070 }
1071
1072 # Add type-specific fields
@@ -1102,11 +1126,13 @@ def deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]] =
1126 "system_prompt": task_data.get("system_prompt", ""),
1127 "prompt": task_data.get("prompt", ""),
1128 "attachments": task_data.get("attachments", []),
1129 + "project_name": task_data.get("project_name"),
1130 + "project_color": task_data.get("project_color"),
1131 "created_at": parse_datetime(task_data.get("created_at")),
1132 "updated_at": parse_datetime(task_data.get("updated_at")),
1133 "last_run": parse_datetime(task_data.get("last_run")),
1134 "last_result": task_data.get("last_result"),
1109 - "context_id": task_data.get("context_id")
1135 + "context_id": task_data.get("context_id"),
1136 }
1137
1138 # Add type-specific fields
python/tools/scheduler.py
+31 -4
@@ -10,6 +10,7 @@ from python.helpers.task_scheduler import (
10 )
11 from agent import AgentContext
12 from python.helpers import persist_chat
13 +from python.helpers.projects import get_context_project_name, load_basic_project_data
14
15 DEFAULT_WAIT_TIMEOUT = 300
16
@@ -38,6 +39,20 @@ class SchedulerTool(Tool):
39 else:
40 return Response(message=f"Unknown method '{self.name}:{self.method}'", break_loop=False)
41
42 + def _resolve_project_metadata(self) -> tuple[str | None, str | None]:
43 + context = self.agent.context
44 + if not context:
45 + return (None, None)
46 + project_slug = get_context_project_name(context)
47 + if not project_slug:
48 + return (None, None)
49 + try:
50 + metadata = load_basic_project_data(project_slug)
51 + color = metadata.get("color") or None
52 + except Exception:
53 + color = None
54 + return project_slug, color
55 +
56 async def list_tasks(self, **kwargs) -> Response:
57 state_filter: list[str] | None = kwargs.get("state", None)
58 type_filter: list[str] | None = kwargs.get("type", None)
@@ -153,13 +168,17 @@ class SchedulerTool(Tool):
168 if not re.match(cron_regex, task_schedule.to_crontab()):
169 return Response(message="Invalid cron expression: " + task_schedule.to_crontab(), break_loop=False)
170
171 + project_slug, project_color = self._resolve_project_metadata()
172 +
173 task = ScheduledTask.create(
174 name=name,
175 system_prompt=system_prompt,
176 prompt=prompt,
177 attachments=attachments,
178 schedule=task_schedule,
162 - context_id=None if dedicated_context else self.agent.context.id
179 + context_id=None if dedicated_context else self.agent.context.id,
180 + project_name=project_slug,
181 + project_color=project_color,
182 )
183 await TaskScheduler.get().add_task(task)
184 return Response(message=f"Scheduled task '{name}' created: {task.uuid}", break_loop=False)
@@ -172,13 +191,17 @@ class SchedulerTool(Tool):
191 token: str = str(random.randint(1000000000000000000, 9999999999999999999))
192 dedicated_context: bool = kwargs.get("dedicated_context", False)
193
194 + project_slug, project_color = self._resolve_project_metadata()
195 +
196 task = AdHocTask.create(
197 name=name,
198 system_prompt=system_prompt,
199 prompt=prompt,
200 attachments=attachments,
201 token=token,
181 - context_id=None if dedicated_context else self.agent.context.id
202 + context_id=None if dedicated_context else self.agent.context.id,
203 + project_name=project_slug,
204 + project_color=project_color,
205 )
206 await TaskScheduler.get().add_task(task)
207 return Response(message=f"Adhoc task '{name}' created: {task.uuid}", break_loop=False)
@@ -206,6 +229,8 @@ class SchedulerTool(Tool):
229 done=[]
230 )
231
232 + project_slug, project_color = self._resolve_project_metadata()
233 +
234 # Create planned task with task plan
235 task = PlannedTask.create(
236 name=name,
@@ -213,7 +238,9 @@ class SchedulerTool(Tool):
238 prompt=prompt,
239 attachments=attachments,
240 plan=task_plan,
216 - context_id=None if dedicated_context else self.agent.context.id
241 + context_id=None if dedicated_context else self.agent.context.id,
242 + project_name=project_slug,
243 + project_color=project_color
244 )
245 await TaskScheduler.get().add_task(task)
246 return Response(message=f"Planned task '{name}' created: {task.uuid}", break_loop=False)
@@ -229,7 +256,7 @@ class SchedulerTool(Tool):
256 return Response(message=f"Task not found: {task_uuid}", break_loop=False)
257
258 if task.context_id == self.agent.context.id:
232 - return Response(message="You can only wait for tasks running in a different chat context (dedicated_context=True).", break_loop=False)
259 + return Response(message="You can only wait for tasks running in their own dedicated context.", break_loop=False)
260
261 done = False
262 elapsed = 0
webui/components/projects/projects-store.js
+43 -21
@@ -106,19 +106,30 @@ const model = {
106
107 async activateProject(name) {
108 try {
109 - await api.callJsonApi("projects", {
109 + const response = await api.callJsonApi("projects", {
110 action: "activate",
111 context_id: chatsStore.getSelectedChatId(),
112 name: name,
113 });
114 - notifications.toastFrontendSuccess(
115 - "Project activated successfully",
116 - "Project activated",
117 - 3,
118 - "projects",
119 - notifications.NotificationPriority.NORMAL,
120 - true
121 - );
114 + if (response?.ok) {
115 + notifications.toastFrontendSuccess(
116 + "Project activated successfully",
117 + "Project activated",
118 + 3,
119 + "projects",
120 + notifications.NotificationPriority.NORMAL,
121 + true
122 + );
123 + } else {
124 + notifications.toastFrontendWarning(
125 + response?.error || "Project activation reported issues",
126 + "Project activation",
127 + 5,
128 + "projects",
129 + notifications.NotificationPriority.NORMAL,
130 + true
131 + );
132 + }
133 } catch (error) {
134 console.error("Error activating project:", error);
135 notifications.toastFrontendError(
@@ -135,18 +146,29 @@ const model = {
146
147 async deactivateProject() {
148 try {
138 - await api.callJsonApi("projects", {
149 + const response = await api.callJsonApi("projects", {
150 action: "deactivate",
151 context_id: chatsStore.getSelectedChatId(),
152 });
142 - notifications.toastFrontendSuccess(
143 - "Project deactivated successfully",
144 - "Project deactivated",
145 - 3,
146 - "projects",
147 - notifications.NotificationPriority.NORMAL,
148 - true
149 - );
153 + if (response?.ok) {
154 + notifications.toastFrontendSuccess(
155 + "Project deactivated successfully",
156 + "Project deactivated",
157 + 3,
158 + "projects",
159 + notifications.NotificationPriority.NORMAL,
160 + true
161 + );
162 + } else {
163 + notifications.toastFrontendWarning(
164 + response?.error || "Project deactivation reported issues",
165 + "Project deactivated",
166 + 5,
167 + "projects",
168 + notifications.NotificationPriority.NORMAL,
169 + true
170 + );
171 + }
172 } catch (error) {
173 console.error("Error deactivating project:", error);
174 notifications.toastFrontendError(
@@ -188,9 +210,9 @@ const model = {
210 );
211 await this.loadProjectsList();
212 } else {
191 - notifications.toastFrontendError(
192 - response.error || "Error deleting project",
193 - "Error deleting project",
213 + notifications.toastFrontendWarning(
214 + response.error || "Project deletion blocked",
215 + "Project delete",
216 5,
217 "projects",
218 notifications.NotificationPriority.NORMAL,
webui/index.css
+9
@@ -1679,3 +1679,12 @@ nav ul li a img {
1679 [data-bs-toggle="collapse"].collapsed .arrow-icon {
1680 transform: rotate(0deg);
1681 }
1682 +
1683 +.project-color-ball {
1684 + width: 0.6em;
1685 + height: 0.6em;
1686 + border-radius: 50%;
1687 + display: inline-block;
1688 + box-sizing: border-box;
1689 + flex-shrink: 0;
1690 +}
webui/index.html
+53 -1
@@ -393,6 +393,34 @@
393 </select>
394 </div>
395
396 + <div class="scheduler-form-field">
397 + <div class="label-help-wrapper">
398 + <label class="scheduler-form-label">Project</label>
399 + <div class="scheduler-form-help" x-text="editingTask.dedicated_context ? 'Inherited from the active chat project.' : 'Mirrors the shared context project.'"></div>
400 + </div>
401 + <template x-if="isCreating">
402 + <div class="project-selector">
403 + <span class="project-color-ball"
404 + :style="editingTask.project?.color ? { backgroundColor: editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span>
405 + <select class="scheduler-project-select"
406 + x-model="selectedProjectSlug"
407 + @change="onProjectSelect($event.target.value)">
408 + <option value="">No project</option>
409 + <template x-for="proj in projectOptions" :key="proj.name">
410 + <option :value="proj.name" x-text="proj.title"></option>
411 + </template>
412 + </select>
413 + </div>
414 + </template>
415 + <template x-if="!isCreating">
416 + <div class="project-display">
417 + <span class="project-color-ball"
418 + :style="editingTask.project?.color ? { backgroundColor: editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span>
419 + <span x-text="formatProjectLabel(editingTask.project)"></span>
420 + </div>
421 + </template>
422 + </div>
423 +
424 <!-- Task State in Create Form - Add after Task Type -->
425 <div class="scheduler-form-field" x-show="isCreating">
426 <div class="label-help-wrapper">
@@ -662,6 +690,17 @@
690 </select>
691 </div>
692
693 + <div class="scheduler-form-field">
694 + <div class="label-help-wrapper">
695 + <label class="scheduler-form-label">Project</label>
696 + <div class="scheduler-form-help" x-text="editingTask.dedicated_context ? 'Dedicated tasks inherit the active chat project.' : 'Shared tasks mirror the project assigned to their context.'"></div>
697 + </div>
698 + <div class="project-display">
699 + <span class="project-color-ball" :style="editingTask.project?.color ? { backgroundColor: editingTask.project.color } : { border: '1px solid var(--color-border)' }"></span>
700 + <span x-text="formatProjectLabel(editingTask.project)"></span>
701 + </div>
702 + </div>
703 +
704 <!-- Task State in Edit Form - Add after Task Type -->
705 <div class="scheduler-form-field" x-show="isEditing">
706 <div class="label-help-wrapper">
@@ -959,6 +998,7 @@
998 :class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑</span>
999 </th>
1000 <th>Type</th>
1001 + <th>Project</th>
1002 <th>Schedule</th>
1003 <th @click="changeSort('last_run')">
1004 Last Run
@@ -981,6 +1021,11 @@
1021 x-text="task.state"></span>
1022 </td>
1023 <td x-text="task.type"></td>
1024 + <td>
1025 + <span class="project-color-ball"
1026 + :style="extractTaskProject(task)?.color ? { backgroundColor: extractTaskProject(task).color } : { border: '1px solid var(--color-border)' }"></span>
1027 + <span x-text="formatTaskProject(task)"></span>
1028 + </td>
1029 <td>
1030 <span x-show="task.type === 'scheduled'"
1031 x-text="formatSchedule(task)"></span>
@@ -1054,6 +1099,13 @@
1099 <div class="scheduler-details-value"
1100 x-text="selectedTaskForDetail ? selectedTaskForDetail.type : ''"></div>
1101
1102 + <div class="scheduler-details-label">Project:</div>
1103 + <div class="scheduler-details-value">
1104 + <span class="project-color-ball"
1105 + :style="extractTaskProject(selectedTaskForDetail)?.color ? { backgroundColor: extractTaskProject(selectedTaskForDetail).color } : { border: '1px solid var(--color-border)' }"></span>
1106 + <span x-text="selectedTaskForDetail ? formatTaskProject(selectedTaskForDetail) : 'No Project'"></span>
1107 + </div>
1108 +
1109 <div class="scheduler-details-label">Created:</div>
1110 <div class="scheduler-details-value"
1111 x-text="selectedTaskForDetail ? formatDate(selectedTaskForDetail.created_at) : ''">
@@ -1199,4 +1251,4 @@
1251
1252 </body>
1253
1202 -</html>
\ No newline at end of file
1254 +</html>
webui/js/scheduler.js
+166 -24
@@ -6,6 +6,7 @@
6 import { formatDateTime, getUserTimezone } from './time-utils.js';
7 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"
8 import { store as notificationsStore } from "/components/notifications/notification-store.js"
9 +import { store as projectsStore } from "/components/projects/projects-store.js"
10
11 // Ensure the showToast function is available
12 // if (typeof window.showToast !== 'function') {
@@ -107,8 +108,12 @@ const fullComponentImplementation = function() {
108 },
109 system_prompt: '',
110 prompt: '',
110 - attachments: []
111 + attachments: [],
112 + project: null,
113 + dedicated_context: true,
114 },
115 + projectOptions: [],
116 + selectedProjectSlug: '',
117 isCreating: false,
118 isEditing: false,
119 showLoadingState: false,
@@ -185,8 +190,11 @@ const fullComponentImplementation = function() {
190 },
191 system_prompt: '',
192 prompt: '',
188 - attachments: []
193 + attachments: [],
194 + project: null,
195 + dedicated_context: true,
196 };
197 + this.refreshProjectOptions();
198
199 // Initialize Flatpickr for date/time pickers after Alpine is fully initialized
200 this.$nextTick(() => {
@@ -439,11 +447,107 @@ const fullComponentImplementation = function() {
447 }
448 },
449
450 + deriveActiveProject() {
451 + const selected = chatsStore?.selectedContext || null;
452 + if (!selected || !selected.project) {
453 + return null;
454 + }
455 +
456 + const project = selected.project;
457 + return {
458 + name: project.name || null,
459 + title: project.title || project.name || null,
460 + color: project.color || '',
461 + };
462 + },
463 +
464 + formatProjectName(project) {
465 + if (!project) {
466 + return 'No Project';
467 + }
468 + const title = project.title || project.name;
469 + return title || 'No Project';
470 + },
471 +
472 + formatProjectLabel(project) {
473 + return `Project: ${this.formatProjectName(project)}`;
474 + },
475 +
476 + async refreshProjectOptions() {
477 + try {
478 + if (!Array.isArray(projectsStore.projectList) || !projectsStore.projectList.length) {
479 + if (typeof projectsStore.loadProjectsList === 'function') {
480 + await projectsStore.loadProjectsList();
481 + }
482 + }
483 + } catch (error) {
484 + console.warn('schedulerSettings: failed to load project list', error);
485 + }
486 +
487 + const list = Array.isArray(projectsStore.projectList) ? projectsStore.projectList : [];
488 + this.projectOptions = list.map((proj) => ({
489 + name: proj.name,
490 + title: proj.title || proj.name,
491 + color: proj.color || '',
492 + }));
493 + },
494 +
495 + onProjectSelect(slug) {
496 + this.selectedProjectSlug = slug || '';
497 + if (!slug) {
498 + this.editingTask.project = null;
499 + return;
500 + }
501 +
502 + const option = this.projectOptions.find((item) => item.name === slug);
503 + if (option) {
504 + this.editingTask.project = { ...option };
505 + } else {
506 + this.editingTask.project = {
507 + name: slug,
508 + title: slug,
509 + color: '',
510 + };
511 + }
512 + },
513 +
514 + extractTaskProject(task) {
515 + if (!task) {
516 + return null;
517 + }
518 +
519 + const slug = task.project_name || null;
520 + const project = task.project || {};
521 + const title = project.name || slug;
522 + const color = task.project_color || project.color || '';
523 +
524 + if (!slug && !title) {
525 + return null;
526 + }
527 +
528 + return {
529 + name: slug,
530 + title: title || slug,
531 + color: color,
532 + };
533 + },
534 +
535 + formatTaskProject(task) {
536 + return this.formatProjectName(this.extractTaskProject(task));
537 + },
538 +
539 // Create a new task
443 - startCreateTask() {
540 + async startCreateTask() {
541 this.isCreating = true;
542 this.isEditing = false;
543 document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'creating');
544 + await this.refreshProjectOptions();
545 + const activeProject = this.deriveActiveProject();
546 + let initialProject = activeProject ? { ...activeProject } : null;
547 + if (!initialProject && this.projectOptions.length > 0) {
548 + initialProject = { ...this.projectOptions[0] };
549 + }
550 +
551 this.editingTask = {
552 name: '',
553 type: 'scheduled', // Default to scheduled
@@ -465,7 +569,10 @@ const fullComponentImplementation = function() {
569 system_prompt: '',
570 prompt: '',
571 attachments: [], // Always initialize as an empty array
572 + project: initialProject,
573 + dedicated_context: true,
574 };
575 + this.selectedProjectSlug = initialProject && initialProject.name ? initialProject.name : '';
576
577 // Set up Flatpickr after the component is visible
578 this.$nextTick(() => {
@@ -487,6 +594,16 @@ const fullComponentImplementation = function() {
594
595 // Create a deep copy to avoid modifying the original
596 this.editingTask = JSON.parse(JSON.stringify(task));
597 + const projectSlug = task.project_name || null;
598 + const projectDisplay = (task.project && task.project.name) || projectSlug;
599 + const projectColor = task.project_color || (task.project ? task.project.color : '') || '';
600 + this.editingTask.project = projectSlug || projectDisplay ? {
601 + name: projectSlug,
602 + title: projectDisplay,
603 + color: projectColor,
604 + } : null;
605 + this.editingTask.dedicated_context = !!task.dedicated_context;
606 + this.selectedProjectSlug = this.editingTask.project && this.editingTask.project.name ? this.editingTask.project.name : '';
607
608 // Debug log
609 console.log('Task data for editing:', task);
@@ -651,7 +768,10 @@ const fullComponentImplementation = function() {
768 system_prompt: '',
769 prompt: '',
770 attachments: [], // Always initialize as an empty array
771 + project: null,
772 + dedicated_context: true,
773 };
774 + this.selectedProjectSlug = '';
775 this.isCreating = false;
776 this.isEditing = false;
777 document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
@@ -678,6 +798,15 @@ const fullComponentImplementation = function() {
798 timezone: getUserTimezone()
799 };
800
801 + if (this.isCreating && this.editingTask.project) {
802 + if (this.editingTask.project.name) {
803 + taskData.project_name = this.editingTask.project.name;
804 + }
805 + if (this.editingTask.project.color) {
806 + taskData.project_color = this.editingTask.project.color;
807 + }
808 + }
809 +
810 // Process attachments - now always stored as array
811 taskData.attachments = Array.isArray(this.editingTask.attachments)
812 ? this.editingTask.attachments
@@ -867,7 +996,9 @@ const fullComponentImplementation = function() {
996 },
997 system_prompt: '',
998 prompt: '',
870 - attachments: []
999 + attachments: [],
1000 + project: null,
1001 + dedicated_context: true,
1002 };
1003 this.isCreating = false;
1004 this.isEditing = false;
@@ -892,12 +1023,15 @@ const fullComponentImplementation = function() {
1023 })
1024 });
1025
1026 + const data = await response.json();
1027 +
1028 if (!response.ok) {
896 - const errorData = await response.json();
897 - throw new Error(errorData.error || 'Failed to run task');
1029 + throw new Error(data?.error || 'Failed to run task');
1030 }
1031
900 - showToast('Task started successfully', 'success');
1032 + const toastMessage = data.warning || data.message || 'Task started successfully';
1033 + const toastType = data.warning ? 'warning' : 'success';
1034 + showToast(toastMessage, toastType);
1035
1036 // Refresh task list
1037 this.fetchTasks();
@@ -1454,25 +1588,21 @@ if (!window.schedulerSettings) {
1588 // Get the full implementation
1589 const fullImpl = fullComponentImplementation();
1590
1457 - // Add essential methods directly
1458 - const essentialMethods = [
1459 - 'fetchTasks', 'startPolling', 'stopPolling',
1460 - 'startCreateTask', 'startEditTask', 'cancelEdit',
1461 - 'saveTask', 'runTask', 'resetTaskState', 'deleteTask',
1462 - 'toggleTaskExpand', 'showTaskDetail', 'closeTaskDetail',
1463 - 'changeSort', 'formatDate', 'formatPlan', 'formatSchedule',
1464 - 'getStateBadgeClass', 'generateRandomToken', 'testFiltering',
1465 - 'debugTasks', 'sortTasks', 'initFlatpickr', 'initDateTimeInput',
1466 - 'updateTasksUI'
1467 - ];
1468 -
1469 - essentialMethods.forEach(method => {
1470 - if (typeof this[method] !== 'function' && typeof fullImpl[method] === 'function') {
1471 - console.log(`Adding missing method: ${method}`);
1472 - this[method] = fullImpl[method];
1591 + // Register all implementation methods (except init) directly
1592 + Object.keys(fullImpl).forEach((key) => {
1593 + if (key === 'init') {
1594 + return;
1595 + }
1596 + if (typeof fullImpl[key] === 'function') {
1597 + console.log(`Registering method: ${key}`);
1598 + this[key] = fullImpl[key];
1599 }
1600 });
1601
1602 + if (typeof this.refreshProjectOptions === 'function') {
1603 + this.refreshProjectOptions();
1604 + }
1605 +
1606 // hack to expose deleteTask
1607 window.deleteTaskGlobal = this.deleteTask.bind(this);
1608
@@ -1484,6 +1614,14 @@ if (!window.schedulerSettings) {
1614 this.tasks = [];
1615 }
1616
1617 + if (!Array.isArray(this.projectOptions)) {
1618 + this.projectOptions = [];
1619 + }
1620 +
1621 + if (typeof this.selectedProjectSlug !== 'string') {
1622 + this.selectedProjectSlug = '';
1623 + }
1624 +
1625 // Make sure attachmentsText getter/setter are defined
1626 if (!Object.getOwnPropertyDescriptor(this, 'attachmentsText')?.get) {
1627 Object.defineProperty(this, 'attachmentsText', {
@@ -1498,7 +1636,11 @@ if (!window.schedulerSettings) {
1636 },
1637 set: function(value) {
1638 if (!this.editingTask) {
1501 - this.editingTask = { attachments: [] };
1639 + this.editingTask = {
1640 + attachments: [],
1641 + project: null,
1642 + dedicated_context: true,
1643 + };
1644 }
1645
1646 if (typeof value === 'string') {