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') {