feat: replace PlanningTool with project-scoped Goal & Task management#44
Conversation
- Added goal_task_routes.py to handle API routes for goals and tasks, including fetching project goals, tasks, and kanban views. - Removed plan_routes.py as its functionality is now covered by the new goal and task routes. - Updated server.py to route requests to the new goal and task endpoints. - Refactored streaming.py to remove plan-related logic and ensure it focuses on goal and task management. - Created goal_tool.py for managing goals, including setting, pausing, resuming, and clearing goals. - Implemented task_create_tool.py, task_list_tool.py, and task_update_tool.py for creating, listing, and updating tasks respectively. - Updated plan_hooks.py to inject active goal and task information into system reminders. - Removed planning_tool.py as its functionality has been replaced by the new goal and task tools. - Updated registry.py to include new tools for goal and task management.
…d update translations for goals and tasks
…grate project board functionality with task management, including task creation, updates, and deletions; enhance UI for better user experience and accessibility
…n): enhance button variants, integrate project task count, and improve UI components for better user experience
…button components for improved UI consistency and functionality
…visibility during new chats
There was a problem hiding this comment.
Pull request overview
Replaces the legacy PlanningTool / PlanModel / TaskModel system with a project-scoped Goal + Task architecture. Backend introduces new ORM models, CRUD routes, and four agent tools (manage_goal, create_tasks, update_task, list_tasks), while the frontend swaps PlanView/PlanProgress/usePlan for a GoalTaskView sidebar plus a fullscreen ProjectKanbanView overlay backed by a new useGoalTasks context. Goals/tasks are keyed on project_id with chat_id used as owner/assignee; blocked is derived from blocked_by at serialization time. A destructive migration drops the old plans table and recreates tasks if it lacks project_id.
Changes:
- New
GoalModel/TaskModel+goals/tasksDB mixins,/project/goal,/project/tasks,/project/kanbanroutes, and goal/task agent tools. - Frontend rewrites:
GoalTaskView,ProjectKanbanView,useGoalTasksprovider, RightSidebar refactor (icon strip always visible, hidden in new-chat state),plan_refreshSSE just triggers refetch. - Destructive migration drops legacy
plans/taskstables; removes plan watcher and auto-completion instreaming.py; updatesdefault_toolsconfig.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/suzent/tools/{goal_tool,task_create_tool,task_update_tool,task_list_tool}.py | New agent tools for managing project goals/tasks |
| src/suzent/tools/planning_tool.py | Removed legacy planning tool |
| src/suzent/tools/plan_hooks.py | Rewrites reminder hook to surface goal + active tasks and increment turn counter |
| src/suzent/tools/registry.py | Registers new tools, drops PlanningTool |
| src/suzent/streaming.py | Removes plan watcher and auto-complete; plan_refresh becomes a bare refresh signal |
| src/suzent/server.py | Replaces /plans and /plan with /project/goal, /project/tasks, /project/kanban |
| src/suzent/routes/plan_routes.py | Removed |
| src/suzent/routes/goal_task_routes.py | New CRUD endpoints for goals and tasks |
| src/suzent/plan.py | Removed |
| src/suzent/database/{models,facade,init,chats,migrations}.py | Replaces Plan/Task ORM with Goal/Task; adds destructive migration |
| src/suzent/database/{goals,tasks}.py | New ORM mixins |
| src/suzent/database/plans.py | Removed |
| src/suzent/config/init.py & config/default.example.yaml | Updated default tool list |
| frontend/src/types/api.ts | Adds Goal/Task types, drops Plan/PlanPhase |
| frontend/src/hooks/{usePlan,useGoalTasks}.ts | Replaces plan context with goal/task context (chat+project scoped) |
| frontend/src/components/sidebar/{PlanView,GoalTaskView,ProjectKanbanView}.tsx | Replaces PlanView with goal/task sidebar + fullscreen kanban |
| frontend/src/components/{PlanProgress,ChatWindow}.tsx | Removes PlanProgress, wires new context, fullscreen board overlay |
| frontend/src/components/chat/RightSidebar.tsx | Restructures sidebar layout, always-visible icon strip, hides in new chat |
| frontend/src/components/BrutalButton.tsx | Adds dark variant |
| frontend/src/App.tsx | Switches PlanProvider → GoalTasksProvider, refreshes both contexts on chat change |
| frontend/src/i18n/messages/{en,zh-CN}.ts | Translation updates for new goal/task/board UI |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def create_project_task(request: Request) -> JSONResponse: | ||
| """Create a task on a project (human-operated board). | ||
|
|
||
| Body JSON: { project_id, title, description?, status?, chat_id? } | ||
| """ | ||
| try: | ||
| body = await request.json() | ||
| project_id = body.get("project_id") | ||
| title = (body.get("title") or "").strip() | ||
| if not project_id or not title: | ||
| return JSONResponse( | ||
| {"error": "project_id and title are required"}, status_code=400 | ||
| ) | ||
| db = get_database() | ||
| task = db.create_task( | ||
| project_id=project_id, | ||
| title=title, | ||
| description=body.get("description") or "", | ||
| chat_id=body.get("chat_id"), | ||
| assignee=body.get("chat_id") or "human", | ||
| ) | ||
| return JSONResponse(_task_to_dict(task), status_code=201) | ||
| except Exception as e: | ||
| return JSONResponse({"error": str(e)}, status_code=500) |
| tasks: Annotated[ | ||
| List[TaskInput], | ||
| Field( | ||
| description="List of tasks to create. Each task has title, description, and optional assignee/blocks/blocked_by." | ||
| ), | ||
| ], |
| table_names = set(inspector.get_table_names()) | ||
| needs_recreate = False | ||
| with self.engine.connect() as conn: | ||
| if "plans" in table_names: | ||
| conn.execute(text("DROP TABLE plans")) | ||
| conn.commit() | ||
| needs_recreate = True | ||
| if "tasks" in table_names: | ||
| task_cols = [col["name"] for col in inspector.get_columns("tasks")] | ||
| if "project_id" not in task_cols: | ||
| conn.execute(text("DROP TABLE tasks")) | ||
| conn.commit() | ||
| needs_recreate = True | ||
| if needs_recreate: | ||
| from sqlmodel import SQLModel | ||
|
|
||
| SQLModel.metadata.create_all(self.engine) |
| if status is not None: | ||
| updates["status"] = status | ||
| if status == "completed": | ||
| updates["completed_at"] = datetime.now(timezone.utc) | ||
| elif status in ("pending", "in_progress"): | ||
| updates["completed_at"] = None |
| const hasGoalContent = Boolean(goal !== null || tasks.length > 0); | ||
| const hasKanbanContent = Boolean(onProjectBoardChange); |
| turns_info = "" | ||
| if goal.max_turns: | ||
| remaining = goal.max_turns - goal.turns_elapsed | ||
| turns_info = f" ({goal.turns_elapsed}/{goal.max_turns} turns used, {remaining} remaining)" | ||
| parts.append(f"[ACTIVE GOAL] {goal.objective}{turns_info}") | ||
| for sg in goal.subgoals: | ||
| parts.append(f" - {sg}") | ||
| parts.append( | ||
| "Evaluate: if the goal is achieved call manage_goal(action='clear'). Otherwise keep working." | ||
| ) | ||
| db.update_goal(goal.id, turns_elapsed=goal.turns_elapsed + 1) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 965b2b2838
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ctx: RunContext[AgentDeps], | ||
| status: Annotated[ | ||
| Optional[ | ||
| Literal["pending", "in_progress", "completed", "blocked", "cancelled"] |
There was a problem hiding this comment.
Return tasks when filtering for blocked status
When the agent calls list_tasks(status='blocked'), this schema advertises blocked as a valid filter value, but the database never stores that status; the API derives it from pending plus a non-empty blocked_by list in goal_task_routes._task_to_dict. Because the tool forwards the value to db.list_tasks, the query becomes TaskModel.status == 'blocked' and real blocked tasks are returned as “No tasks found.” Handle blocked specially or remove it from the accepted filter values.
Useful? React with 👍 / 👎.
| ] = None, | ||
| ) -> ToolResult: | ||
| db = get_database() | ||
| task = db.get_task(task_id) |
There was a problem hiding this comment.
Validate task ownership before updating by id
In a chat linked to one project, a model can still call update_task with a task id from another project, for example from stale conversation history or a copied id. This fetches by global id and applies updates without comparing task.project_id to the current chat’s resolved project, so project-scoped task management can mutate another project’s board. Resolve the current project here and reject tasks whose project_id differs before calling db.update_task.
Useful? React with 👍 / 👎.
- goal_task_routes: honour status field from POST body on task creation - task_create_tool: remove misleading 'assignee' mention from field description - task_update_tool: clear completed_at on any non-completed status transition; add project ownership check before applying updates - task_list_tool: handle derived 'blocked' status filter correctly - plan_hooks: emit hard stop warning when max_turns budget is exhausted - RightSidebar: remove unused hasKanbanContent variable
Summary
PlanningTool/PlanModel/TaskModelsystem with a new project-scoped Goal and Task architectureGoalModel,TaskModel, CRUD routes (/project/goal,/project/tasks,/project/kanban), and agent tools (goal_tool,create_tasks,update_task,list_tasks). Goals and tasks are scoped toproject_idwithchat_idas the ownership/assignee markerPlanView/usePlanwithGoalTaskView+ProjectKanbanView+useGoalTaskscontext. The Goal tab shows current chat's checklist; the full project Kanban opens as a fullscreen overlay. Human-operable board with inline add, click-to-cycle status, and deleteKey design decisions
blockedstatus is derived at serialisation time fromblocked_byarray — not stored in DB or accepted as tool inputassignee=chat_id; displayed in kanban as shortened ID + chat title via deterministic unicode shape hashplan_refreshSSE event triggers frontend refresh after stream endsTest plan