From b7f9afdb1dece31f64862437f74e8a2a5551f237 Mon Sep 17 00:00:00 2001 From: Rafael Uzarowski Date: Thu, 13 Nov 2025 00:20:24 +0100 Subject: [PATCH 1/2] feat: Project Scheduler Tasks --- python/api/poll.py | 5 +- python/api/scheduler_task_create.py | 37 +++- python/api/scheduler_task_update.py | 4 + python/api/scheduler_tasks_list.py | 4 +- python/helpers/task_scheduler.py | 42 ++++- python/tools/scheduler.py | 35 +++- webui/components/projects/projects-store.js | 64 ++++--- webui/index.css | 9 + webui/index.html | 54 +++++- webui/js/scheduler.js | 190 +++++++++++++++++--- 10 files changed, 378 insertions(+), 66 deletions(-) diff --git a/python/api/poll.py b/python/api/poll.py index eb021c2f29..561759dde4 100644 --- a/python/api/poll.py +++ b/python/api/poll.py @@ -85,6 +85,9 @@ async def process(self, input: dict, request: Request) -> dict | Response: "last_result": task_details.get("last_result"), "attachments": task_details.get("attachments", []), "context_id": task_details.get("context_id"), + "project_name": task_details.get("project_name"), + "project_color": task_details.get("project_color"), + "project": task_details.get("project"), }) # Add type-specific fields @@ -119,4 +122,4 @@ async def process(self, input: dict, request: Request) -> dict | Response: "notifications": notifications, "notifications_guid": notification_manager.guid, "notifications_version": len(notification_manager.updates), - } \ No newline at end of file + } diff --git a/python/api/scheduler_task_create.py b/python/api/scheduler_task_create.py index c091b3b198..48aeb24e89 100644 --- a/python/api/scheduler_task_create.py +++ b/python/api/scheduler_task_create.py @@ -3,6 +3,7 @@ TaskScheduler, ScheduledTask, AdHocTask, PlannedTask, TaskSchedule, serialize_task, parse_task_schedule, parse_task_plan, TaskType ) +from python.helpers.projects import load_basic_project_data from python.helpers.localization import Localization from python.helpers.print_style import PrintStyle import random @@ -27,7 +28,26 @@ async def process(self, input: Input, request: Request) -> Output: system_prompt = input.get("system_prompt", "") prompt = input.get("prompt") attachments = input.get("attachments", []) - context_id = input.get("context_id", None) + + requested_project_slug = input.get("project_name") + if isinstance(requested_project_slug, str): + requested_project_slug = requested_project_slug.strip() or None + else: + requested_project_slug = None + + project_slug = requested_project_slug + project_color = None + + if project_slug: + try: + metadata = load_basic_project_data(requested_project_slug) + project_color = metadata.get("color") or None + except Exception as exc: + printer.error(f"SchedulerTaskCreate: failed to load project '{project_slug}': {exc}") + return {"error": f"Saving project failed: {project_slug}"} + + # Always dedicated context for scheduler tasks created by ui + task_context_id = None # Check if schedule is provided (for ScheduledTask) schedule = input.get("schedule", {}) @@ -77,8 +97,10 @@ async def process(self, input: Input, request: Request) -> Output: prompt=prompt, schedule=task_schedule, attachments=attachments, - context_id=context_id, - timezone=timezone + context_id=task_context_id, + timezone=timezone, + project_name=project_slug, + project_color=project_color, ) elif plan: # Create a planned task @@ -94,7 +116,9 @@ async def process(self, input: Input, request: Request) -> Output: prompt=prompt, plan=task_plan, attachments=attachments, - context_id=context_id + context_id=task_context_id, + project_name=project_slug, + project_color=project_color, ) else: # Create an ad-hoc task @@ -105,7 +129,9 @@ async def process(self, input: Input, request: Request) -> Output: prompt=prompt, token=token, attachments=attachments, - context_id=context_id + context_id=task_context_id, + project_name=project_slug, + project_color=project_color, ) # Verify token after creation if isinstance(task, AdHocTask): @@ -132,5 +158,6 @@ async def process(self, input: Input, request: Request) -> Output: printer.print(f"Serialized adhoc task, token in response: '{task_dict.get('token')}'") return { + "ok": True, "task": task_dict } diff --git a/python/api/scheduler_task_update.py b/python/api/scheduler_task_update.py index 433738652e..b5b73cb59a 100644 --- a/python/api/scheduler_task_update.py +++ b/python/api/scheduler_task_update.py @@ -48,6 +48,9 @@ async def process(self, input: Input, request: Request) -> Output: if "attachments" in input: update_params["attachments"] = input.get("attachments", []) + if "project_name" in input or "project_color" in input: + return {"error": "Project changes are not allowed"} + # Update schedule if this is a scheduled task and schedule is provided if isinstance(task, ScheduledTask) and "schedule" in input: schedule_data = input.get("schedule", {}) @@ -85,5 +88,6 @@ async def process(self, input: Input, request: Request) -> Output: task_dict = serialize_task(updated_task) return { + "ok": True, "task": task_dict } diff --git a/python/api/scheduler_tasks_list.py b/python/api/scheduler_tasks_list.py index 30a8c3f068..8d07235d23 100644 --- a/python/api/scheduler_tasks_list.py +++ b/python/api/scheduler_tasks_list.py @@ -22,8 +22,8 @@ async def process(self, input: Input, request: Request) -> Output: # Use the scheduler's convenience method for task serialization tasks_list = scheduler.serialize_all_tasks() - return {"tasks": tasks_list} + return {"ok": True, "tasks": tasks_list} except Exception as e: PrintStyle.error(f"Failed to list tasks: {str(e)} {traceback.format_exc()}") - return {"error": f"Failed to list tasks: {str(e)} {traceback.format_exc()}", "tasks": []} + return {"ok": False, "error": f"Failed to list tasks: {str(e)} {traceback.format_exc()}", "tasks": []} diff --git a/python/helpers/task_scheduler.py b/python/helpers/task_scheduler.py index 819ee6058b..53da2c94f8 100644 --- a/python/helpers/task_scheduler.py +++ b/python/helpers/task_scheduler.py @@ -124,6 +124,8 @@ class BaseTask(BaseModel): system_prompt: str prompt: str attachments: list[str] = Field(default_factory=list) + project_name: str | None = Field(default=None) + project_color: str | None = Field(default=None) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) last_run: datetime | None = None @@ -181,6 +183,9 @@ def check_schedule(self, frequency_seconds: float = 60.0) -> bool: def get_next_run(self) -> datetime | None: return None + def is_dedicated(self) -> bool: + return self.context_id == self.uuid + def get_next_run_minutes(self) -> int | None: next_run = self.get_next_run() if next_run is None: @@ -243,14 +248,18 @@ def create( prompt: str, token: str, attachments: list[str] = list(), - context_id: str | None = None + context_id: str | None = None, + project_name: str | None = None, + project_color: str | None = None ): return cls(name=name, system_prompt=system_prompt, prompt=prompt, attachments=attachments, token=token, - context_id=context_id) + context_id=context_id, + project_name=project_name, + project_color=project_color) def update(self, name: str | None = None, @@ -288,7 +297,9 @@ def create( schedule: TaskSchedule, attachments: list[str] = list(), context_id: str | None = None, - timezone: str | None = None + timezone: str | None = None, + project_name: str | None = None, + project_color: str | None = None, ): # Set timezone in schedule if provided if timezone is not None: @@ -301,7 +312,9 @@ def create( prompt=prompt, attachments=attachments, schedule=schedule, - context_id=context_id) + context_id=context_id, + project_name=project_name, + project_color=project_color) def update(self, name: str | None = None, @@ -365,14 +378,18 @@ def create( prompt: str, plan: TaskPlan, attachments: list[str] = list(), - context_id: str | None = None + context_id: str | None = None, + project_name: str | None = None, + project_color: str | None = None ): return cls(name=name, system_prompt=system_prompt, prompt=prompt, plan=plan, attachments=attachments, - context_id=context_id) + context_id=context_id, + project_name=project_name, + project_color=project_color) def update(self, name: str | None = None, @@ -1037,12 +1054,19 @@ def serialize_task(task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> Dict[s "system_prompt": task.system_prompt, "prompt": task.prompt, "attachments": task.attachments, + "project_name": task.project_name, + "project_color": task.project_color, "created_at": serialize_datetime(task.created_at), "updated_at": serialize_datetime(task.updated_at), "last_run": serialize_datetime(task.last_run), "next_run": serialize_datetime(task.get_next_run()), "last_result": task.last_result, - "context_id": task.context_id + "context_id": task.context_id, + "dedicated_context": task.is_dedicated(), + "project": { + "name": task.project_name, + "color": task.project_color, + }, } # Add type-specific fields @@ -1102,11 +1126,13 @@ def deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]] = "system_prompt": task_data.get("system_prompt", ""), "prompt": task_data.get("prompt", ""), "attachments": task_data.get("attachments", []), + "project_name": task_data.get("project_name"), + "project_color": task_data.get("project_color"), "created_at": parse_datetime(task_data.get("created_at")), "updated_at": parse_datetime(task_data.get("updated_at")), "last_run": parse_datetime(task_data.get("last_run")), "last_result": task_data.get("last_result"), - "context_id": task_data.get("context_id") + "context_id": task_data.get("context_id"), } # Add type-specific fields diff --git a/python/tools/scheduler.py b/python/tools/scheduler.py index 61b613de87..96c4530246 100644 --- a/python/tools/scheduler.py +++ b/python/tools/scheduler.py @@ -10,6 +10,7 @@ ) from agent import AgentContext from python.helpers import persist_chat +from python.helpers.projects import get_context_project_name, load_basic_project_data DEFAULT_WAIT_TIMEOUT = 300 @@ -38,6 +39,20 @@ async def execute(self, **kwargs): else: return Response(message=f"Unknown method '{self.name}:{self.method}'", break_loop=False) + def _resolve_project_metadata(self) -> tuple[str | None, str | None]: + context = self.agent.context + if not context: + return (None, None) + project_slug = get_context_project_name(context) + if not project_slug: + return (None, None) + try: + metadata = load_basic_project_data(project_slug) + color = metadata.get("color") or None + except Exception: + color = None + return project_slug, color + async def list_tasks(self, **kwargs) -> Response: state_filter: list[str] | None = kwargs.get("state", None) type_filter: list[str] | None = kwargs.get("type", None) @@ -153,13 +168,17 @@ async def create_scheduled_task(self, **kwargs) -> Response: if not re.match(cron_regex, task_schedule.to_crontab()): return Response(message="Invalid cron expression: " + task_schedule.to_crontab(), break_loop=False) + project_slug, project_color = self._resolve_project_metadata() + task = ScheduledTask.create( name=name, system_prompt=system_prompt, prompt=prompt, attachments=attachments, schedule=task_schedule, - context_id=None if dedicated_context else self.agent.context.id + context_id=None if dedicated_context else self.agent.context.id, + project_name=project_slug, + project_color=project_color, ) await TaskScheduler.get().add_task(task) return Response(message=f"Scheduled task '{name}' created: {task.uuid}", break_loop=False) @@ -172,13 +191,17 @@ async def create_adhoc_task(self, **kwargs) -> Response: token: str = str(random.randint(1000000000000000000, 9999999999999999999)) dedicated_context: bool = kwargs.get("dedicated_context", False) + project_slug, project_color = self._resolve_project_metadata() + task = AdHocTask.create( name=name, system_prompt=system_prompt, prompt=prompt, attachments=attachments, token=token, - context_id=None if dedicated_context else self.agent.context.id + context_id=None if dedicated_context else self.agent.context.id, + project_name=project_slug, + project_color=project_color, ) await TaskScheduler.get().add_task(task) return Response(message=f"Adhoc task '{name}' created: {task.uuid}", break_loop=False) @@ -206,6 +229,8 @@ async def create_planned_task(self, **kwargs) -> Response: done=[] ) + project_slug, project_color = self._resolve_project_metadata() + # Create planned task with task plan task = PlannedTask.create( name=name, @@ -213,7 +238,9 @@ async def create_planned_task(self, **kwargs) -> Response: prompt=prompt, attachments=attachments, plan=task_plan, - context_id=None if dedicated_context else self.agent.context.id + context_id=None if dedicated_context else self.agent.context.id, + project_name=project_slug, + project_color=project_color ) await TaskScheduler.get().add_task(task) return Response(message=f"Planned task '{name}' created: {task.uuid}", break_loop=False) @@ -229,7 +256,7 @@ async def wait_for_task(self, **kwargs) -> Response: return Response(message=f"Task not found: {task_uuid}", break_loop=False) if task.context_id == self.agent.context.id: - return Response(message="You can only wait for tasks running in a different chat context (dedicated_context=True).", break_loop=False) + return Response(message="You can only wait for tasks running in their own dedicated context.", break_loop=False) done = False elapsed = 0 diff --git a/webui/components/projects/projects-store.js b/webui/components/projects/projects-store.js index 420fc21e7b..97a03022f2 100644 --- a/webui/components/projects/projects-store.js +++ b/webui/components/projects/projects-store.js @@ -105,19 +105,30 @@ const model = { async activateProject(name) { try { - await api.callJsonApi("projects", { + const response = await api.callJsonApi("projects", { action: "activate", context_id: chatsStore.getSelectedChatId(), name: name, }); - notifications.toastFrontendSuccess( - "Project activated successfully", - "Project activated", - 3, - "projects", - notifications.NotificationPriority.NORMAL, - true - ); + if (response?.ok) { + notifications.toastFrontendSuccess( + "Project activated successfully", + "Project activated", + 3, + "projects", + notifications.NotificationPriority.NORMAL, + true + ); + } else { + notifications.toastFrontendWarning( + response?.error || "Project activation reported issues", + "Project activation", + 5, + "projects", + notifications.NotificationPriority.NORMAL, + true + ); + } } catch (error) { console.error("Error activating project:", error); notifications.toastFrontendError( @@ -134,18 +145,29 @@ const model = { async deactivateProject() { try { - await api.callJsonApi("projects", { + const response = await api.callJsonApi("projects", { action: "deactivate", context_id: chatsStore.getSelectedChatId(), }); - notifications.toastFrontendSuccess( - "Project deactivated successfully", - "Project deactivated", - 3, - "projects", - notifications.NotificationPriority.NORMAL, - true - ); + if (response?.ok) { + notifications.toastFrontendSuccess( + "Project deactivated successfully", + "Project deactivated", + 3, + "projects", + notifications.NotificationPriority.NORMAL, + true + ); + } else { + notifications.toastFrontendWarning( + response?.error || "Project deactivation reported issues", + "Project deactivated", + 5, + "projects", + notifications.NotificationPriority.NORMAL, + true + ); + } } catch (error) { console.error("Error deactivating project:", error); notifications.toastFrontendError( @@ -187,9 +209,9 @@ const model = { ); await this.loadProjectsList(); } else { - notifications.toastFrontendError( - response.error || "Error deleting project", - "Error deleting project", + notifications.toastFrontendWarning( + response.error || "Project deletion blocked", + "Project delete", 5, "projects", notifications.NotificationPriority.NORMAL, diff --git a/webui/index.css b/webui/index.css index 5d65555266..204a063d37 100644 --- a/webui/index.css +++ b/webui/index.css @@ -1677,3 +1677,12 @@ nav ul li a img { [data-bs-toggle="collapse"].collapsed .arrow-icon { transform: rotate(0deg); } + +.project-color-ball { + width: 0.6em; + height: 0.6em; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + flex-shrink: 0; +} diff --git a/webui/index.html b/webui/index.html index 7b62b4f2bf..141ad9d41d 100644 --- a/webui/index.html +++ b/webui/index.html @@ -393,6 +393,34 @@

+
+
+ +
+
+ + +
+
@@ -662,6 +690,17 @@

+
+
+ +
+
+
+ + +
+
+
@@ -959,6 +998,7 @@

Task Management

:class="{'scheduler-sort-desc': sortDirection === 'desc'}">↑ Type + Project Schedule Last Run @@ -981,6 +1021,11 @@

Task Management

x-text="task.state"> + + + + @@ -1054,6 +1099,13 @@

Task Management

+
Project:
+
+ + +
+
Created:
@@ -1199,4 +1251,4 @@

Task Management

- \ No newline at end of file + diff --git a/webui/js/scheduler.js b/webui/js/scheduler.js index e09ab328cf..96cb548dde 100644 --- a/webui/js/scheduler.js +++ b/webui/js/scheduler.js @@ -6,6 +6,7 @@ import { formatDateTime, getUserTimezone } from './time-utils.js'; import { store as chatsStore } from "/components/sidebar/chats/chats-store.js" import { store as notificationsStore } from "/components/notifications/notification-store.js" +import { store as projectsStore } from "/components/projects/projects-store.js" // Ensure the showToast function is available // if (typeof window.showToast !== 'function') { @@ -107,8 +108,12 @@ const fullComponentImplementation = function() { }, system_prompt: '', prompt: '', - attachments: [] + attachments: [], + project: null, + dedicated_context: true, }, + projectOptions: [], + selectedProjectSlug: '', isCreating: false, isEditing: false, showLoadingState: false, @@ -185,8 +190,11 @@ const fullComponentImplementation = function() { }, system_prompt: '', prompt: '', - attachments: [] + attachments: [], + project: null, + dedicated_context: true, }; + this.refreshProjectOptions(); // Initialize Flatpickr for date/time pickers after Alpine is fully initialized this.$nextTick(() => { @@ -439,11 +447,107 @@ const fullComponentImplementation = function() { } }, + deriveActiveProject() { + const selected = chatsStore?.selectedContext || null; + if (!selected || !selected.project) { + return null; + } + + const project = selected.project; + return { + name: project.name || null, + title: project.title || project.name || null, + color: project.color || '', + }; + }, + + formatProjectName(project) { + if (!project) { + return 'No Project'; + } + const title = project.title || project.name; + return title || 'No Project'; + }, + + formatProjectLabel(project) { + return `Project: ${this.formatProjectName(project)}`; + }, + + async refreshProjectOptions() { + try { + if (!Array.isArray(projectsStore.projectList) || !projectsStore.projectList.length) { + if (typeof projectsStore.loadProjectsList === 'function') { + await projectsStore.loadProjectsList(); + } + } + } catch (error) { + console.warn('schedulerSettings: failed to load project list', error); + } + + const list = Array.isArray(projectsStore.projectList) ? projectsStore.projectList : []; + this.projectOptions = list.map((proj) => ({ + name: proj.name, + title: proj.title || proj.name, + color: proj.color || '', + })); + }, + + onProjectSelect(slug) { + this.selectedProjectSlug = slug || ''; + if (!slug) { + this.editingTask.project = null; + return; + } + + const option = this.projectOptions.find((item) => item.name === slug); + if (option) { + this.editingTask.project = { ...option }; + } else { + this.editingTask.project = { + name: slug, + title: slug, + color: '', + }; + } + }, + + extractTaskProject(task) { + if (!task) { + return null; + } + + const slug = task.project_name || null; + const project = task.project || {}; + const title = project.name || slug; + const color = task.project_color || project.color || ''; + + if (!slug && !title) { + return null; + } + + return { + name: slug, + title: title || slug, + color: color, + }; + }, + + formatTaskProject(task) { + return this.formatProjectName(this.extractTaskProject(task)); + }, + // Create a new task - startCreateTask() { + async startCreateTask() { this.isCreating = true; this.isEditing = false; document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'creating'); + await this.refreshProjectOptions(); + const activeProject = this.deriveActiveProject(); + let initialProject = activeProject ? { ...activeProject } : null; + if (!initialProject && this.projectOptions.length > 0) { + initialProject = { ...this.projectOptions[0] }; + } + this.editingTask = { name: '', type: 'scheduled', // Default to scheduled @@ -465,7 +569,10 @@ const fullComponentImplementation = function() { system_prompt: '', prompt: '', attachments: [], // Always initialize as an empty array + project: initialProject, + dedicated_context: true, }; + this.selectedProjectSlug = initialProject && initialProject.name ? initialProject.name : ''; // Set up Flatpickr after the component is visible this.$nextTick(() => { @@ -487,6 +594,16 @@ const fullComponentImplementation = function() { // Create a deep copy to avoid modifying the original this.editingTask = JSON.parse(JSON.stringify(task)); + const projectSlug = task.project_name || null; + const projectDisplay = (task.project && task.project.name) || projectSlug; + const projectColor = task.project_color || (task.project ? task.project.color : '') || ''; + this.editingTask.project = projectSlug || projectDisplay ? { + name: projectSlug, + title: projectDisplay, + color: projectColor, + } : null; + this.editingTask.dedicated_context = !!task.dedicated_context; + this.selectedProjectSlug = this.editingTask.project && this.editingTask.project.name ? this.editingTask.project.name : ''; // Debug log console.log('Task data for editing:', task); @@ -651,7 +768,10 @@ const fullComponentImplementation = function() { system_prompt: '', prompt: '', attachments: [], // Always initialize as an empty array + project: null, + dedicated_context: true, }; + this.selectedProjectSlug = ''; this.isCreating = false; this.isEditing = false; document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state'); @@ -678,6 +798,15 @@ const fullComponentImplementation = function() { timezone: getUserTimezone() }; + if (this.isCreating && this.editingTask.project) { + if (this.editingTask.project.name) { + taskData.project_name = this.editingTask.project.name; + } + if (this.editingTask.project.color) { + taskData.project_color = this.editingTask.project.color; + } + } + // Process attachments - now always stored as array taskData.attachments = Array.isArray(this.editingTask.attachments) ? this.editingTask.attachments @@ -867,7 +996,9 @@ const fullComponentImplementation = function() { }, system_prompt: '', prompt: '', - attachments: [] + attachments: [], + project: null, + dedicated_context: true, }; this.isCreating = false; this.isEditing = false; @@ -892,12 +1023,15 @@ const fullComponentImplementation = function() { }) }); + const data = await response.json(); + if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to run task'); + throw new Error(data?.error || 'Failed to run task'); } - showToast('Task started successfully', 'success'); + const toastMessage = data.warning || data.message || 'Task started successfully'; + const toastType = data.warning ? 'warning' : 'success'; + showToast(toastMessage, toastType); // Refresh task list this.fetchTasks(); @@ -1454,25 +1588,21 @@ if (!window.schedulerSettings) { // Get the full implementation const fullImpl = fullComponentImplementation(); - // Add essential methods directly - const essentialMethods = [ - 'fetchTasks', 'startPolling', 'stopPolling', - 'startCreateTask', 'startEditTask', 'cancelEdit', - 'saveTask', 'runTask', 'resetTaskState', 'deleteTask', - 'toggleTaskExpand', 'showTaskDetail', 'closeTaskDetail', - 'changeSort', 'formatDate', 'formatPlan', 'formatSchedule', - 'getStateBadgeClass', 'generateRandomToken', 'testFiltering', - 'debugTasks', 'sortTasks', 'initFlatpickr', 'initDateTimeInput', - 'updateTasksUI' - ]; - - essentialMethods.forEach(method => { - if (typeof this[method] !== 'function' && typeof fullImpl[method] === 'function') { - console.log(`Adding missing method: ${method}`); - this[method] = fullImpl[method]; + // Register all implementation methods (except init) directly + Object.keys(fullImpl).forEach((key) => { + if (key === 'init') { + return; + } + if (typeof fullImpl[key] === 'function') { + console.log(`Registering method: ${key}`); + this[key] = fullImpl[key]; } }); + if (typeof this.refreshProjectOptions === 'function') { + this.refreshProjectOptions(); + } + // hack to expose deleteTask window.deleteTaskGlobal = this.deleteTask.bind(this); @@ -1484,6 +1614,14 @@ if (!window.schedulerSettings) { this.tasks = []; } + if (!Array.isArray(this.projectOptions)) { + this.projectOptions = []; + } + + if (typeof this.selectedProjectSlug !== 'string') { + this.selectedProjectSlug = ''; + } + // Make sure attachmentsText getter/setter are defined if (!Object.getOwnPropertyDescriptor(this, 'attachmentsText')?.get) { Object.defineProperty(this, 'attachmentsText', { @@ -1498,7 +1636,11 @@ if (!window.schedulerSettings) { }, set: function(value) { if (!this.editingTask) { - this.editingTask = { attachments: [] }; + this.editingTask = { + attachments: [], + project: null, + dedicated_context: true, + }; } if (typeof value === 'string') { From ec3438a00b193510217da82ddf15a2c44caaa611 Mon Sep 17 00:00:00 2001 From: Rafael Uzarowski Date: Thu, 13 Nov 2025 00:30:22 +0100 Subject: [PATCH 2/2] fix: task chats payload fix in poll() --- python/api/poll.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/api/poll.py b/python/api/poll.py index 561759dde4..d549307fa8 100644 --- a/python/api/poll.py +++ b/python/api/poll.py @@ -87,7 +87,10 @@ async def process(self, input: dict, request: Request) -> dict | Response: "context_id": task_details.get("context_id"), "project_name": task_details.get("project_name"), "project_color": task_details.get("project_color"), - "project": task_details.get("project"), + "project": { + "name": task_details.get("project_name"), + "color": task_details.get("project_color"), + }, }) # Add type-specific fields