From dbdd6e8cb795f5b24b36480e6085edd488aa4d81 Mon Sep 17 00:00:00 2001 From: haikdc Date: Sun, 5 Apr 2026 11:30:53 -0700 Subject: [PATCH] [Haik]: ckpt, copied over legacy modes subapp --- backend/apps/modes/__init__.py | 0 backend/apps/modes/builtin.py | 137 +++++++++++++++++++++++++++++++++ backend/apps/modes/models.py | 38 +++++++++ backend/apps/modes/modes.py | 90 ++++++++++++++++++++++ 4 files changed, 265 insertions(+) create mode 100644 backend/apps/modes/__init__.py create mode 100644 backend/apps/modes/builtin.py create mode 100644 backend/apps/modes/models.py create mode 100644 backend/apps/modes/modes.py diff --git a/backend/apps/modes/__init__.py b/backend/apps/modes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/modes/builtin.py b/backend/apps/modes/builtin.py new file mode 100644 index 00000000..a7f3a0f7 --- /dev/null +++ b/backend/apps/modes/builtin.py @@ -0,0 +1,137 @@ +"""Built-in mode definitions. + +Separated from models.py to keep schema classes small and data separate. +""" + +from backend.apps.modes.models import Mode +from backend.config.paths import OUTPUTS_WORKSPACE_DIR as OUTPUTS_WORKSPACE, SKILLS_WORKSPACE_DIR as SKILLS_WORKSPACE + +BUILTIN_MODES: list[Mode] = [ + Mode( + id="agent", + name="Agent", + description="Full autonomous agent with read and write access to tools.", + system_prompt=None, + tools=None, + default_next_mode=None, + is_builtin=True, + icon="smart_toy", + color="#818cf8", + ), + Mode( + id="ask", + name="Ask", + description="Answer questions about the codebase. Read-only, no edits or changes.", + system_prompt="Answer questions about the codebase. Do not make any edits or changes.", + tools=["Read", "Glob", "Grep", "AskUserQuestion"], + default_next_mode=None, + is_builtin=True, + icon="question_answer", + color="#4ade80", + ), + Mode( + id="plan", + name="Plan", + description="Analyze requests and produce a detailed step-by-step plan without executing.", + system_prompt="Analyze the request and produce a detailed step-by-step plan. Do not execute the plan or make any changes.", + tools=["Read", "Glob", "Grep", "AskUserQuestion"], + default_next_mode="agent", + is_builtin=True, + icon="map", + color="#fbbf24", + ), + Mode( + id="view-builder", + name="App Builder", + description="Create and iterate on reusable App artifacts.", + system_prompt=( + "You are an App Builder — an AI assistant that creates self-contained " + "web apps rendered in an iframe preview.\n\n" + "Your working directory is a dedicated workspace folder pre-seeded with " + "template files. Read the existing files before making changes.\n\n" + "## Critical rules\n\n" + "- The entry point MUST be named `index.html`. Never rename it or create " + "a different HTML file as the main entry point.\n" + "- Write files immediately when you have code ready — the user sees a " + "live preview that auto-refreshes from these files.\n" + "- Always write the complete file content on first creation (do not use " + "Edit for partial patches on new files).\n" + "- For complex apps, split code into separate files (JS, CSS, etc.) " + "and reference them from index.html with relative paths.\n" + "- Always update meta.json with a short name and one-sentence description.\n" + "- Build beautiful, polished UIs with modern design — dark themes, smooth " + "transitions, proper spacing, and responsive layouts.\n\n" + "Read the SKILL.md reference in your workspace for the full technical " + "specification of the App platform (available globals, file conventions, " + "schema format, backend.py usage, and examples)." + ), + tools=None, + default_next_mode=None, + is_builtin=True, + icon="view_quilt", + color="#f472b6", + default_folder=OUTPUTS_WORKSPACE, + ), + Mode( + id="skill-builder", + name="Skill Builder", + description="Create and iterate on skills using AI-assisted vibe coding.", + system_prompt=( + "You are a Skill Builder — an AI assistant that helps users create, " + "refine, and iterate on Claude skills (SKILL.md files).\n\n" + "## How Skills Work\n\n" + "A skill is a Markdown file that teaches Claude how to perform a specific task. " + "Skills have YAML frontmatter with `name` and `description` fields, followed by " + "the skill body in Markdown. The description is the primary triggering mechanism — " + "it tells Claude when to use the skill.\n\n" + "## Your Working Directory\n\n" + "Your working directory is a dedicated workspace folder for this skill. " + "Write your output directly to these files using the Write tool:\n\n" + "1. **SKILL.md** — The complete skill file with YAML frontmatter and Markdown body. " + "Example frontmatter:\n" + " ```\n" + " ---\n" + " name: my-skill\n" + " description: When to trigger and what this skill does.\n" + " ---\n" + " ```\n\n" + "2. **meta.json** — Metadata for the skill builder UI. Always write this file. Example:\n" + ' {"name":"My Skill","description":"A short description","command":"my-skill"}\n\n' + "Write these files immediately when you have content ready. The user can see " + "a live preview that auto-refreshes from these files. Always write the " + "complete file content (do not use Edit for partial patches on first creation).\n\n" + "## Skill Creation Process\n\n" + "1. **Understand intent** — Ask what the skill should do, when it should trigger, " + "and what the expected output format is.\n" + "2. **Draft the skill** — Write a SKILL.md with clear instructions, examples, " + "and good progressive disclosure.\n" + "3. **Iterate** — Refine based on user feedback. Update the files each time.\n\n" + "## Skill Writing Best Practices\n\n" + "- Keep SKILL.md under 500 lines; use bundled reference files for large content.\n" + "- The `description` frontmatter is the primary trigger. Make it slightly \"pushy\" — " + "include both what the skill does AND specific contexts for when to use it.\n" + "- Use imperative form in instructions.\n" + "- Include examples with input/output pairs when helpful.\n" + "- Define output formats explicitly with templates.\n" + "- Use theory of mind — explain *why* things matter rather than just MUST directives.\n" + "- Think about edge cases, error handling, and progressive disclosure.\n\n" + "## Skill Anatomy\n\n" + "```\n" + "skill-name/\n" + "├── SKILL.md (required) — YAML frontmatter + Markdown instructions\n" + "└── Bundled Resources (optional)\n" + " ├── scripts/ — Executable code for repetitive tasks\n" + " ├── references/ — Docs loaded into context as needed\n" + " └── assets/ — Files used in output\n" + "```\n\n" + "Be collaborative and flexible. If the user wants to \"just vibe\", skip the formal " + "process and iterate freely. Always write updated files so the preview stays current." + ), + tools=None, + default_next_mode=None, + is_builtin=True, + icon="psychology", + color="#10b981", + default_folder=SKILLS_WORKSPACE, + ), +] diff --git a/backend/apps/modes/models.py b/backend/apps/modes/models.py new file mode 100644 index 00000000..f63b155d --- /dev/null +++ b/backend/apps/modes/models.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field +from typing import Optional +from uuid import uuid4 + + +class Mode(BaseModel): + id: str = Field(default_factory=lambda: uuid4().hex) + name: str + description: str = "" + system_prompt: Optional[str] = None + tools: Optional[list[str]] = None + default_next_mode: Optional[str] = None + is_builtin: bool = False + icon: str = "smart_toy" + color: str = "#818cf8" + default_folder: Optional[str] = None + + +class ModeCreate(BaseModel): + name: str + description: str = "" + system_prompt: Optional[str] = None + tools: Optional[list[str]] = None + default_next_mode: Optional[str] = None + icon: str = "smart_toy" + color: str = "#818cf8" + default_folder: Optional[str] = None + + +class ModeUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + system_prompt: Optional[str] = None + tools: Optional[list[str]] = None + default_next_mode: Optional[str] = None + icon: Optional[str] = None + color: Optional[str] = None + default_folder: Optional[str] = None diff --git a/backend/apps/modes/modes.py b/backend/apps/modes/modes.py new file mode 100644 index 00000000..b7c1a695 --- /dev/null +++ b/backend/apps/modes/modes.py @@ -0,0 +1,90 @@ +import json +import os +import logging +from contextlib import asynccontextmanager +from fastapi import HTTPException +from backend.config.Apps import SubApp +from backend.apps.common.json_store import JsonStore +from backend.apps.modes.models import Mode, ModeCreate, ModeUpdate +from backend.apps.modes.builtin import BUILTIN_MODES + +logger = logging.getLogger(__name__) + +from backend.config.paths import MODES_DIR as DATA_DIR + + +@asynccontextmanager +async def modes_lifespan(): + os.makedirs(DATA_DIR, exist_ok=True) + for builtin in BUILTIN_MODES: + path = os.path.join(DATA_DIR, f"{builtin.id}.json") + if not os.path.exists(path): + _save(builtin) + yield + + +modes = SubApp("modes", modes_lifespan) + + +_store = JsonStore(Mode, DATA_DIR, not_found_detail="Mode not found") + +_load_all = _store.load_all +_save = _store.save +_load = _store.load +load_mode = _store.load_or_none + + +@modes.router.get("/list") +async def list_modes(): + builtin_defaults = {m.id: m.model_dump() for m in BUILTIN_MODES} + return {"modes": [m.model_dump() for m in _load_all()], "builtin_defaults": builtin_defaults} + + +@modes.router.get("/{mode_id}") +async def get_mode(mode_id: str): + return _load(mode_id).model_dump() + + +@modes.router.post("/create") +async def create_mode(body: ModeCreate): + mode = Mode( + name=body.name, + description=body.description, + system_prompt=body.system_prompt, + tools=body.tools, + default_next_mode=body.default_next_mode, + icon=body.icon, + color=body.color, + default_folder=body.default_folder, + is_builtin=False, + ) + _save(mode) + return {"ok": True, "mode": mode.model_dump()} + + +@modes.router.put("/{mode_id}") +async def update_mode(mode_id: str, body: ModeUpdate): + mode = _load(mode_id) + for k, v in body.model_dump(exclude_unset=True).items(): + setattr(mode, k, v) + _save(mode) + return {"ok": True, "mode": mode.model_dump()} + + +@modes.router.post("/{mode_id}/reset") +async def reset_mode(mode_id: str): + """Reset a built-in mode to its hardcoded defaults.""" + builtin = next((m for m in BUILTIN_MODES if m.id == mode_id), None) + if not builtin: + raise HTTPException(status_code=400, detail="Only built-in modes can be reset") + _save(builtin) + return {"ok": True, "mode": builtin.model_dump()} + + +@modes.router.delete("/{mode_id}") +async def delete_mode(mode_id: str): + mode = _load(mode_id) + if mode.is_builtin: + raise HTTPException(status_code=403, detail="Cannot delete built-in modes") + _store.delete(mode_id) + return {"ok": True}