From 0efb0d4f8fcdec70215de203e44987589ae534bb Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:59:39 +0200 Subject: [PATCH] Improve scheduler prompt editing and support optional YAML Give task prompts full-width, resizable fields and an expanded view in the shared modal stack. Preserve drafts on failed saves and keep actions in the pinned footer. Honor the selected initial task state and inherit only the active chat's project. Support existing tasks.yaml files through the existing helpers, with JSON remaining the default and taking precedence. Do not migrate existing task files or change scheduler execution logic. Validation: - 28 focused scheduler, timezone, and tool-contract checks passed. - 8 checks passed in the Docker framework runtime. - Live desktop/mobile creation, editing, deletion, and storage checks. --- api/scheduler_task_create.py | 4 +- api/scheduler_task_create.py.dox.md | 3 +- helpers/task_scheduler.py | 27 +- helpers/task_scheduler.py.dox.md | 3 + tests/test_scheduler_editor.py | 241 ++++++++++++++++++ webui/components/modals/AGENTS.md | 3 + .../scheduler/scheduler-prompt-editor.html | 37 +++ .../modals/scheduler/scheduler-store.js | 26 +- .../scheduler/scheduler-task-editor.html | 163 ++++++------ 9 files changed, 409 insertions(+), 98 deletions(-) create mode 100644 tests/test_scheduler_editor.py create mode 100644 webui/components/modals/scheduler/scheduler-prompt-editor.html diff --git a/api/scheduler_task_create.py b/api/scheduler_task_create.py index 31522d377..0868a535c 100644 --- a/api/scheduler_task_create.py +++ b/api/scheduler_task_create.py @@ -1,7 +1,7 @@ from helpers.api import ApiHandler, Input, Output, Request from helpers.task_scheduler import ( TaskScheduler, ScheduledTask, AdHocTask, PlannedTask, TaskSchedule, - serialize_task, parse_task_schedule, parse_task_plan, TaskType + serialize_task, parse_task_schedule, parse_task_plan, TaskType, TaskState ) from helpers.projects import load_basic_project_data from helpers.localization import Localization @@ -27,6 +27,7 @@ class SchedulerTaskCreate(ApiHandler): name = input.get("name") system_prompt = input.get("system_prompt", "") prompt = input.get("prompt") + state = TaskState(input.get("state", TaskState.IDLE)) attachments = input.get("attachments", []) requested_project_slug = input.get("project_name") @@ -138,6 +139,7 @@ class SchedulerTaskCreate(ApiHandler): printer.print(f"AdHocTask created with token: '{task.token}'") # Add the task to the scheduler + task.state = state await scheduler.add_task(task) # Verify the task was added correctly - retrieve by UUID to check persistence diff --git a/api/scheduler_task_create.py.dox.md b/api/scheduler_task_create.py.dox.md index 332118085..286c2dc75 100644 --- a/api/scheduler_task_create.py.dox.md +++ b/api/scheduler_task_create.py.dox.md @@ -20,6 +20,7 @@ - Update this file whenever request payloads, authentication or CSRF requirements, response shapes, route side effects, or WebSocket event contracts change. - `SchedulerTaskCreate` is an `ApiHandler`. - `SchedulerTaskCreate` defines `process(...)`. +- An optional `state` is validated as `TaskState` and applied before persistence; it defaults to `idle`. Creating a disabled task must not briefly expose it as runnable. - Observed side-effect areas: filesystem writes, secret handling, scheduler state. - Imported dependency areas include: `helpers.api`, `helpers.localization`, `helpers.print_style`, `helpers.projects`, `helpers.task_scheduler`, `random`. @@ -37,7 +38,7 @@ ## Verification - Run endpoint-specific or API/WebSocket tests for changed behavior; smoke-test browser callers when no focused test exists. -- No direct test reference was found by name search; choose the nearest behavioral test or perform a focused smoke check. +- Run `tests/test_scheduler_editor.py` for initial-state persistence. ## Child DOX Index diff --git a/helpers/task_scheduler.py b/helpers/task_scheduler.py index fada618e9..356423ecf 100644 --- a/helpers/task_scheduler.py +++ b/helpers/task_scheduler.py @@ -22,7 +22,7 @@ from helpers.print_style import PrintStyle from helpers.defer import DeferredTask from helpers.files import get_abs_path, make_dirs, read_file, write_file from helpers.localization import Localization -from helpers import projects, guids +from helpers import projects, guids, yaml import pytz from typing import Annotated @@ -509,6 +509,12 @@ class PlannedTask(BaseTask): await super().on_error(error) +def _task_file_path() -> str: + json_path = get_abs_path(SCHEDULER_FOLDER, "tasks.json") + yaml_path = get_abs_path(SCHEDULER_FOLDER, "tasks.yaml") + return yaml_path if not exists(json_path) and exists(yaml_path) else json_path + + class SchedulerTaskList(BaseModel): tasks: list[Annotated[Union[ScheduledTask, AdHocTask, PlannedTask], Field(discriminator="type")]] = Field(default_factory=list) # Singleton instance @@ -518,26 +524,33 @@ class SchedulerTaskList(BaseModel): @classmethod def get(cls) -> "SchedulerTaskList": - path = get_abs_path(SCHEDULER_FOLDER, "tasks.json") + path = _task_file_path() if cls.__instance is None: if not exists(path): make_dirs(path) cls.__instance = asyncio.run(cls(tasks=[]).save()) else: - cls.__instance = cls.model_validate_json(read_file(path)) + cls.__instance = cls._load(path) else: asyncio.run(cls.__instance.reload()) return cls.__instance + @classmethod + def _load(cls, path: str) -> "SchedulerTaskList": + content = read_file(path) + if path.endswith(".yaml"): + return cls.model_validate(yaml.loads(content)) + return cls.model_validate_json(content) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._lock = threading.RLock() async def reload(self) -> "SchedulerTaskList": - path = get_abs_path(SCHEDULER_FOLDER, "tasks.json") + path = _task_file_path() if exists(path): with self._lock: - data = self.__class__.model_validate_json(read_file(path)) + data = self.__class__._load(path) self.tasks.clear() self.tasks.extend(data.tasks) return self @@ -563,7 +576,7 @@ class SchedulerTaskList(BaseModel): f"Fixed: Generated new token '{task.token}' for task {task.name}" ) - path = get_abs_path(SCHEDULER_FOLDER, "tasks.json") + path = _task_file_path() if not exists(path): make_dirs(path) @@ -576,7 +589,7 @@ class SchedulerTaskList(BaseModel): "ERROR: Found null token in JSON output for an adhoc task" ) - write_file(path, json_data) + write_file(path, yaml.from_json(json_data) if path.endswith(".yaml") else json_data) # Debug: Verify after saving if exists(path): diff --git a/helpers/task_scheduler.py.dox.md b/helpers/task_scheduler.py.dox.md index 7292b4faf..1b5342fcb 100644 --- a/helpers/task_scheduler.py.dox.md +++ b/helpers/task_scheduler.py.dox.md @@ -83,6 +83,8 @@ ## Runtime Contracts +- `SchedulerTaskList` keeps `usr/scheduler/tasks.json` as its default and prefers it when both formats exist. An existing `tasks.yaml` is supported when no JSON file exists; saves use the same selection and format. Loading never migrates, renames, or creates a second file. +- JSON retains its existing Pydantic parser and `helpers.files` persistence path. Optional YAML uses `helpers.yaml` with the same task models. Invalid data raises before replacing the in-memory task list; YAML comments and formatting are regenerated on save. - Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together. - Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change. - Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, network calls, settings/state persistence, secret handling, scheduler state. @@ -104,6 +106,7 @@ - Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers. - Related tests observed by source search: - `tests/test_task_scheduler_timezone.py` + - `tests/test_scheduler_editor.py` - `tests/test_timezone_regressions.py` - `tests/test_tool_action_contracts.py` diff --git a/tests/test_scheduler_editor.py b/tests/test_scheduler_editor.py new file mode 100644 index 000000000..859ca8a27 --- /dev/null +++ b/tests/test_scheduler_editor.py @@ -0,0 +1,241 @@ +"""Scheduler prompt editing and human-editable task persistence.""" +import asyncio +from datetime import datetime, timezone, timedelta +import json +from pathlib import Path +import re +import subprocess +import sys +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch + +import yaml + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from helpers import task_scheduler as scheduler +from api.scheduler_task_create import SchedulerTaskCreate + + +def test_scheduler_json_default_and_optional_yaml(): + common = dict( + name="Prompt editing", system_prompt="First line\n Keep indentation: # sì\n", + prompt="Check these values:\ntrue\n123\n*\n\n", state="disabled", + created_at=datetime(2026, 9, 24, tzinfo=timezone.utc), + updated_at=datetime(2026, 9, 24, tzinfo=timezone.utc), + attachments=["/tmp/report.md"], last_result="A result\nwith two lines", + ) + original = scheduler.SchedulerTaskList(tasks=[ + scheduler.AdHocTask(**common, token="000123"), + scheduler.ScheduledTask(**common, schedule=scheduler.TaskSchedule( + minute="30", hour="9", day="*", month="*", weekday="*", timezone="Europe/Rome", + )), + scheduler.PlannedTask(**common, plan=scheduler.TaskPlan( + todo=[datetime(2026, 10, 1, 9, tzinfo=timezone.utc)], + )), + ]) + expected = original.model_dump(mode="json") + for extension in ("json", "yaml"): + with TemporaryDirectory() as folder, patch.object(scheduler, "SCHEDULER_FOLDER", folder), patch.object( + scheduler.SchedulerTaskList, "_SchedulerTaskList__instance", None, + ): + path = Path(folder, f"tasks.{extension}") + text = original.model_dump_json() if extension == "json" else scheduler.yaml.dumps(expected) + path.write_text(text, encoding="utf-8") + tasks = scheduler.SchedulerTaskList.get() + assert tasks.model_dump(mode="json") == expected + assert path.read_text(encoding="utf-8") == text, "loading must not rewrite the file" + asyncio.run(tasks.save()) + assert list(Path(folder).iterdir()) == [path], "saving must not migrate formats" + assert yaml.safe_load(path.read_text(encoding="utf-8")) == expected + + path.write_text(path.read_text(encoding="utf-8").replace("First line", "Edited externally"), encoding="utf-8") + asyncio.run(tasks.reload()) + assert all(task.system_prompt.startswith("Edited externally\n") for task in tasks.tasks) + before = tasks.model_dump(mode="json") + for invalid in ("tasks: [", "", '{"tasks":{}}', '{"tasks":[{"type":"unknown"}]}', "!!python/object:builtins.object {}"): + path.write_text(invalid, encoding="utf-8") + try: + asyncio.run(tasks.reload()) + except (ValueError, yaml.YAMLError): + pass + else: + raise AssertionError("Invalid task data was accepted") + assert tasks.model_dump(mode="json") == before + assert path.read_text(encoding="utf-8") == invalid + + path.write_text(text, encoding="utf-8") + json_path, yaml_path = Path(folder, "tasks.json"), Path(folder, "tasks.yaml") + json_path.write_text(original.model_dump_json(), encoding="utf-8") + yaml_path.write_text("tasks: []\n", encoding="utf-8") + with patch.object(scheduler.yaml, "loads", side_effect=AssertionError("JSON must not use YAML parsing")): + asyncio.run(tasks.reload()) + assert tasks.model_dump(mode="json") == expected + asyncio.run(tasks.remove_task_by_uuid(tasks.tasks[0].uuid)) + assert len(json.loads(json_path.read_text())["tasks"]) == 2 + assert yaml_path.read_text() == "tasks: []\n", "JSON takes precedence without changing YAML" + + with TemporaryDirectory() as folder, patch.object(scheduler, "SCHEDULER_FOLDER", folder), patch.object( + scheduler.SchedulerTaskList, "_SchedulerTaskList__instance", None, + ): + assert scheduler.SchedulerTaskList.get().tasks == [] + assert json.loads(Path(folder, "tasks.json").read_text()) == {"tasks": []} + assert not Path(folder, "tasks.yaml").exists() + + +def test_scheduler_due_tasks_and_execution_guards_in_both_formats(): + now = datetime(2026, 9, 24, 8, 30, 30, tzinfo=timezone.utc) + common = dict(name="Execution check", system_prompt="", prompt="Check scheduler state") + templates = [ + scheduler.AdHocTask(**common), + scheduler.ScheduledTask(**common, schedule=scheduler.TaskSchedule( + minute="30", hour="10", day="*", month="*", weekday="*", timezone="Europe/Rome", + )), + scheduler.PlannedTask(**common, plan=scheduler.TaskPlan(todo=[now - timedelta(minutes=1)])), + ] + data = scheduler.SchedulerTaskList(tasks=[ + task.__class__.model_validate({**task.model_dump(), "uuid": f"{task.type.value}-{state.value}", "state": state}) + for task in templates for state in scheduler.TaskState + ]) + + async def check(engine): + due = await engine._tasks.get_due_tasks() + assert {task.uuid for task in due} == {"scheduled-idle", "planned-idle"} + for task in list(engine.get_tasks()): + if task.state in (scheduler.TaskState.DISABLED, scheduler.TaskState.RUNNING): + try: + await engine.run_task_by_uuid(task.uuid) + except ValueError: + pass + else: + raise AssertionError("A disabled or running task was started") + else: + await engine.run_task_by_uuid(task.uuid) + assert engine._run_task.call_args.args[0].state == scheduler.TaskState.IDLE + assert engine._run_task.await_count == 6 + + claim = await engine.update_task_checked( + "planned-idle", lambda task: task.state == scheduler.TaskState.IDLE, state=scheduler.TaskState.RUNNING, + ) + assert claim is not None + assert await engine.update_task_checked( + "planned-idle", lambda task: task.state == scheduler.TaskState.IDLE, state=scheduler.TaskState.RUNNING, + ) is None + await claim.on_run() + await claim.on_success("Result\nwith preserved line breaks") + await claim.on_finish() + await engine.reload() + finished = engine.get_task_by_uuid("planned-idle") + assert finished.state == scheduler.TaskState.IDLE + assert finished.last_run == now + assert finished.last_result == "Result\nwith preserved line breaks" + assert finished.plan.todo == [] and finished.plan.in_progress is None + assert finished.plan.done == [now - timedelta(minutes=1)] + assert "planned-idle" not in {task.uuid for task in await engine._tasks.get_due_tasks()} + + failed = engine.get_task_by_uuid("adhoc-idle") + await failed.on_error("Expected failure") + await engine.reload() + assert engine.get_task_by_uuid(failed.uuid).state == scheduler.TaskState.ERROR + assert engine.get_task_by_uuid(failed.uuid).last_result == "ERROR: Expected failure" + + for extension in ("json", "yaml"): + with TemporaryDirectory() as folder, patch.object(scheduler, "SCHEDULER_FOLDER", folder), patch.object( + scheduler, "_now", return_value=now, + ), patch("helpers.state_monitor_integration.mark_dirty_all"): + content = data.model_dump_json() if extension == "json" else scheduler.yaml.from_json(data.model_dump_json()) + Path(folder, f"tasks.{extension}").write_text(content, encoding="utf-8") + engine = object.__new__(scheduler.TaskScheduler) + engine._tasks = asyncio.run(scheduler.SchedulerTaskList().reload()) + engine._run_task = AsyncMock() + with patch.object(scheduler.TaskScheduler, "get", return_value=engine): + asyncio.run(check(engine)) + + +def test_scheduler_creation_applies_state_before_persistence(): + states = [] + fake = SimpleNamespace( + reload=AsyncMock(), add_task=AsyncMock(side_effect=lambda task: states.append(task.state)), + get_task_by_uuid=lambda _uuid: fake.add_task.call_args.args[0], + ) + with patch.object(scheduler.TaskScheduler, "get", return_value=fake): + for config in ({}, {"schedule": "0 9 * * *"}, {"plan": {"todo": ["2026-10-01T09:00:00+00:00"]}}): + for state in ({}, {"state": "disabled"}): + result = asyncio.run(SchedulerTaskCreate(None, None).process({ + "name": "Initial state", "prompt": "Keep disabled until enabled", **config, **state, + }, None)) + assert result["ok"] + assert states[-1] == state.get("state", "idle") == result["task"]["state"] + + +def test_scheduler_editor_preserves_drafts_until_save_succeeds(): + source = (PROJECT_ROOT / "webui/components/modals/scheduler/scheduler-store.js").read_text() + source = re.sub(r"^import\b[\s\S]*?;\n", "", source, flags=re.M) + source = source.replace("export { store };", "") + script = r''' +import assert from 'node:assert/strict'; +const window = { localStorage: { getItem: () => null }, alert: assert.fail }; +const createStore = (_name, model) => model; +const getUserTimezone = () => 'Europe/Rome'; +let response, sent, destroyed = 0; +const errors = [], opened = [], closed = []; +const openModal = async path => opened.push(path); +const closeModal = async path => closed.push(path); +const notificationsStore = { frontendError: message => errors.push(message), frontendSuccess() {} }; +const fetchApi = async (_endpoint, options) => { + sent = JSON.parse(options.body); + if (response instanceof Error) throw response; + return response; +}; +''' + source + r''' +store.destroyFlatpickr = () => destroyed++; +for (const isCreating of [true, false]) { + store.isCreating = isCreating; + store.isEditing = !isCreating; + store.editingTask = defaultEditingTask({ + uuid: 'task-id', type: 'adhoc', name: 'Task', token: '123', + system_prompt: 'Keep\nall instructions', prompt: 'A long prompt\nwith edits\n', + }); + const draft = store.editingTask; + for (const failure of [ + { ok: true, json: async () => ({ error: 'Invalid task' }) }, + { ok: false, json: async () => ({ error: 'Save failed' }) }, + new Error('Network unavailable'), + ]) { + response = failure; + await store.saveTask(); + assert.equal(store.editingTask, draft); + assert.equal(store.isCreating, isCreating); + assert.equal(store.isEditing, !isCreating); + assert.equal(sent.prompt, draft.prompt); + } + for (const field of ['system_prompt', 'prompt']) { + await store.openPromptEditor(field); + assert.equal(store.promptField, field); + store.editingTask[store.promptField] += '\nExpanded edit'; + await store.closePromptEditor(); + assert.ok(draft[field].endsWith('\nExpanded edit')); + assert.equal(opened.at(-1), closed.at(-1)); + } + response = { ok: true, json: async () => ({ ok: true, task: { ...draft } }) }; + await store.saveTask(); + assert.equal(store.isCreating, false); + assert.equal(store.isEditing, false); + assert.equal(store.editingTask.prompt, ''); + assert.equal(store.tasks.find(task => task.uuid === draft.uuid).prompt, draft.prompt); +} +assert.equal(errors.length, 6); +assert.equal(destroyed, 2, 'failed saves must leave the editor and date picker intact'); +await store.openPromptEditor('name'); +assert.equal(opened.length, 4, 'only prompt fields can be expanded'); +store.refreshProjectOptions = async () => {}; +store.projectOptions = [{ name: 'unrelated-project' }]; +store.deriveActiveProject = () => null; +await store.startCreateTask(); +assert.equal(store.selectedProjectSlug, ''); +assert.equal(store.editingTask.project, null, 'no active project must not select an unrelated project'); +''' + subprocess.run(["node", "--input-type=module"], input=script, text=True, check=True) diff --git a/webui/components/modals/AGENTS.md b/webui/components/modals/AGENTS.md index 9e9c710c9..cc0c99e2d 100644 --- a/webui/components/modals/AGENTS.md +++ b/webui/components/modals/AGENTS.md @@ -10,6 +10,8 @@ - Each direct child folder owns one modal workflow and its store. - Modal HTML files own body content, titles, scoped styles, and `data-modal-footer` content. - Modal store files own modal-local state and cleanup. +- `scheduler/` owns task editing: both prompt fields use full-width resizable textareas, with a shared expanded prompt modal bound to the same draft. Closing the expanded view retains draft edits; only a successful task save clears the draft. API error bodies must be treated as failures even with HTTP 200. +- Scheduler actions live in the pinned footer. New tasks offer idle/disabled initial states and inherit only the active chat's project, otherwise defaulting to no project. ## Local Contracts @@ -25,6 +27,7 @@ ## Verification - Smoke-test open, close, Escape, click-outside, scrolling, stacked modals, and pinned footers after modal changes. +- For scheduler editing, run `pytest tests/test_scheduler_editor.py` and check prompt expansion, draft retention, and save/reopen on desktop and mobile. ## Child DOX Index diff --git a/webui/components/modals/scheduler/scheduler-prompt-editor.html b/webui/components/modals/scheduler/scheduler-prompt-editor.html new file mode 100644 index 000000000..77a6cadff --- /dev/null +++ b/webui/components/modals/scheduler/scheduler-prompt-editor.html @@ -0,0 +1,37 @@ + + + Edit Task Prompt + + + +
+ + +
+ + + diff --git a/webui/components/modals/scheduler/scheduler-store.js b/webui/components/modals/scheduler/scheduler-store.js index 40721a1e4..662c5308f 100644 --- a/webui/components/modals/scheduler/scheduler-store.js +++ b/webui/components/modals/scheduler/scheduler-store.js @@ -1,5 +1,6 @@ import { createStore } from "/js/AlpineStore.js"; import { fetchApi } from "/js/api.js"; +import { openModal, closeModal } from "/js/modals.js"; import { formatDateTime, getUserDateTimeParts, @@ -282,7 +283,7 @@ async function callSchedulerEndpoint(endpoint, payload = {}, defaultError) { body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); - if (!response.ok) { + if (!response.ok || data?.error) { return { ok: false, error: data?.error || defaultError || "Task request failed" }; } return { ok: true, data }; @@ -479,6 +480,7 @@ const schedulerStoreModel = { isCreating: false, isEditing: false, editingTask: defaultEditingTask(), + promptField: "prompt", selectedProjectSlug: "", projectOptions: [], @@ -679,12 +681,8 @@ const schedulerStoreModel = { } catch (error) { this.notifyError(`Failed to save task: ${error.message}`); return; - } finally { - this.destroyFlatpickr("all"); - this.resetEditingTask(); - this.isCreating = false; - this.isEditing = false; } + this.cancelEdit(); }, async runTask(taskId) { @@ -756,8 +754,19 @@ const schedulerStoreModel = { }, // Domain helpers ----------------------------------------------------------- + openPromptEditor(field) { + if (!["system_prompt", "prompt"].includes(field)) return; + this.promptField = field; + return openModal("modals/scheduler/scheduler-prompt-editor.html"); + }, + + closePromptEditor() { + return closeModal("modals/scheduler/scheduler-prompt-editor.html"); + }, + resetEditingTask() { this.editingTask = defaultEditingTask(); + this.promptField = "prompt"; this.selectedProjectSlug = ""; }, @@ -1022,10 +1031,7 @@ const schedulerStoreModel = { this.isEditing = false; await this.refreshProjectOptions(); - let initialProject = this.deriveActiveProject(); - if (!initialProject && this.projectOptions.length > 0) { - initialProject = { ...this.projectOptions[0] }; - } + const initialProject = this.deriveActiveProject(); this.editingTask = defaultEditingTask({ token: this.generateRandomToken(), diff --git a/webui/components/modals/scheduler/scheduler-task-editor.html b/webui/components/modals/scheduler/scheduler-task-editor.html index bd09a126f..256c9ac11 100644 --- a/webui/components/modals/scheduler/scheduler-task-editor.html +++ b/webui/components/modals/scheduler/scheduler-task-editor.html @@ -13,15 +13,6 @@
Create New Task
-
-
-
- - -
-
@@ -64,31 +55,13 @@
- +
Select the initial state of the task
-
-
- Idle - Running - Disabled - Error -
-
- Idle: Ready to run - Running: Currently executing - Disabled: Won't execute automatically - Error: Task encountered an error -
-
+
-
-
- -
System-level instructions for the assistant
+
+
+
+ +
System-level instructions for the assistant
+
+
- +
-
-
- -
The main task prompt that will be executed
+
+
+
+ +
The main task prompt that will be executed
+
+
- +
@@ -220,18 +207,6 @@
Edit Task
-
-
- -
-
- - -
-
@@ -391,22 +366,36 @@
-
-
- -
System-level instructions for the assistant
+
+
+
+ +
System-level instructions for the assistant
+
+
- +
-
-
- -
The main task prompt that will be executed
+
+
+
+ +
The main task prompt that will be executed
+
+
- +
@@ -424,7 +413,33 @@
-