mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[Haik]: ckpt, view builder saving issue fixed via templates, system prompts, and skills. Also added a reset to default for modes.
This commit is contained in:
@@ -711,6 +711,11 @@ class AgentManager:
|
||||
global_settings = load_settings()
|
||||
composed_prompt = self._compose_system_prompt(global_settings.default_system_prompt, mode_sys_prompt, session.system_prompt, connected_tools_ctx, outputs_ctx, browser_ctx)
|
||||
|
||||
if session.mode == "view-builder":
|
||||
from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL
|
||||
skill_block = f"<app_builder_reference>\n{VIEW_BUILDER_SKILL}\n</app_builder_reference>"
|
||||
composed_prompt = f"{composed_prompt}\n\n{skill_block}" if composed_prompt else skill_block
|
||||
|
||||
mcp_servers = await self._build_mcp_servers(session.allowed_tools)
|
||||
|
||||
_browser_delegation_tools = ["CreateBrowserAgent", "BrowserAgent", "BrowserAgents"]
|
||||
|
||||
@@ -76,43 +76,28 @@ BUILTIN_MODES: list[Mode] = [
|
||||
),
|
||||
Mode(
|
||||
id="view-builder",
|
||||
name="View Builder",
|
||||
description="Create and iterate on reusable View artifacts.",
|
||||
name="App Builder",
|
||||
description="Create and iterate on reusable App artifacts.",
|
||||
system_prompt=(
|
||||
"You are helping the user build a reusable View — a self-contained "
|
||||
"web app rendered in an iframe.\n\n"
|
||||
"Your working directory is a dedicated workspace folder for this view. "
|
||||
"You can create any file structure you need using the Write tool.\n\n"
|
||||
"## Required files\n\n"
|
||||
"1. **index.html** — The entry point. A complete HTML document. "
|
||||
"React 18 is available via esm.sh CDN imports:\n"
|
||||
' <script type="importmap">{"imports":{"react":"https://esm.sh/react@18",'
|
||||
'"react-dom/client":"https://esm.sh/react-dom@18/client"}}</script>\n'
|
||||
" The structured input data is available at `window.OUTPUT_INPUT` (object) "
|
||||
"and any server-side result at `window.OUTPUT_BACKEND_RESULT`.\n\n"
|
||||
"2. **schema.json** — A JSON Schema object defining the structured input "
|
||||
"the view accepts. Example:\n"
|
||||
' {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}\n\n'
|
||||
"3. **meta.json** — Metadata for this view. Always write this file with "
|
||||
"a short name and one-sentence description. Example:\n"
|
||||
' {"name":"Sales Dashboard","description":"Interactive dashboard showing sales metrics"}\n\n'
|
||||
"## Optional files\n\n"
|
||||
"- **backend.py** — Python code that receives `input_data` as "
|
||||
"a global dict and must assign its result to a global `result` dict.\n"
|
||||
"- **Any additional files** — You can create subdirectories and split code "
|
||||
"across multiple files. For example:\n"
|
||||
" - `components/Chart.js` — Reusable components\n"
|
||||
" - `utils/helpers.js` — Utility functions\n"
|
||||
" - `styles/main.css` — Stylesheets\n\n"
|
||||
"Files are served from the workspace, so relative imports work naturally:\n"
|
||||
' `<script type="module" src="./components/Chart.js"></script>`\n'
|
||||
' `<link rel="stylesheet" href="./styles/main.css">`\n'
|
||||
" `import { helper } from './utils/helpers.js'` (in ES modules)\n\n"
|
||||
"## Guidelines\n\n"
|
||||
"Write files immediately when you have code 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). "
|
||||
"For complex views, split code into separate files to keep things organized."
|
||||
"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,
|
||||
|
||||
@@ -59,7 +59,8 @@ def load_mode(mode_id: str) -> Mode | None:
|
||||
|
||||
@modes.router.get("/list")
|
||||
async def list_modes():
|
||||
return {"modes": [m.model_dump() for m in _load_all()]}
|
||||
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}")
|
||||
@@ -93,6 +94,16 @@ async def update_mode(mode_id: str, body: ModeUpdate):
|
||||
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)
|
||||
|
||||
@@ -15,6 +15,7 @@ from backend.apps.outputs.models import (
|
||||
WorkspaceSeedRequest,
|
||||
)
|
||||
from backend.apps.outputs.executor import execute_backend_code
|
||||
from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL, VIEW_TEMPLATE_FILES
|
||||
from backend.apps.settings.settings import load_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -235,6 +236,14 @@ async def seed_workspace(body: WorkspaceSeedRequest):
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "w") as f:
|
||||
f.write(content)
|
||||
else:
|
||||
for rel_path, content in VIEW_TEMPLATE_FILES.items():
|
||||
full_path = os.path.join(folder, rel_path)
|
||||
with open(full_path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
with open(os.path.join(folder, "SKILL.md"), "w") as f:
|
||||
f.write(VIEW_BUILDER_SKILL)
|
||||
|
||||
if body.meta:
|
||||
with open(os.path.join(folder, "meta.json"), "w") as f:
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# App Builder — Platform Reference
|
||||
|
||||
You are building an **App**: a self-contained web app served in an iframe.
|
||||
The workspace you're working in is the source of truth — every file you write
|
||||
here is served directly to the live preview.
|
||||
|
||||
---
|
||||
|
||||
## File conventions
|
||||
|
||||
| File | Required | Purpose |
|
||||
|------|----------|---------|
|
||||
| `index.html` | **Yes** | Entry point. Must be a complete HTML document. This is the ONLY file the preview iframe loads — never rename it. |
|
||||
| `meta.json` | **Yes** | `{"name":"…","description":"…"}` — displayed in the UI header. Always write this. |
|
||||
| `schema.json` | Recommended | JSON Schema defining the input form (the "Test Input" tab). |
|
||||
| `backend.py` | Optional | Server-side Python executed before rendering. |
|
||||
| Everything else | Optional | JS, CSS, images, subdirectories — referenced from `index.html` via relative paths. |
|
||||
|
||||
### ⚠️ Do NOT
|
||||
|
||||
- Name the main HTML file anything other than `index.html` — the platform
|
||||
will not find it and the preview will be blank.
|
||||
- Use `document.write()` — it breaks the injected data globals.
|
||||
- Assume any external server or API is available unless the user provides one.
|
||||
|
||||
---
|
||||
|
||||
## Injected globals
|
||||
|
||||
Before `index.html` loads, the platform injects two globals:
|
||||
|
||||
```javascript
|
||||
window.OUTPUT_INPUT // Object — structured input from the schema form
|
||||
window.OUTPUT_BACKEND_RESULT // Object | null — result from backend.py execution
|
||||
```
|
||||
|
||||
These are available immediately in any `<script>` tag. You can also listen for
|
||||
live updates when the user changes input:
|
||||
|
||||
```javascript
|
||||
window.addEventListener('output-data-ready', () => {
|
||||
const input = window.OUTPUT_INPUT;
|
||||
const result = window.OUTPUT_BACKEND_RESULT;
|
||||
// re-render with new data
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## schema.json format
|
||||
|
||||
Standard JSON Schema. The platform renders a form from this automatically.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string", "default": "My Dashboard" },
|
||||
"count": { "type": "number", "default": 10 },
|
||||
"enabled": { "type": "boolean", "default": true },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": ["alpha", "beta"]
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
```
|
||||
|
||||
Supported types: `string`, `number`, `integer`, `boolean`, `array`, `object`.
|
||||
Use `"default"` values so the preview works without manual input.
|
||||
|
||||
---
|
||||
|
||||
## backend.py
|
||||
|
||||
Optional server-side Python that runs before the frontend renders.
|
||||
It receives a global `input_data` dict (the schema form values) and must
|
||||
assign its result to a global `result` dict.
|
||||
|
||||
```python
|
||||
# input_data is pre-populated from the schema form
|
||||
import json
|
||||
|
||||
result = {
|
||||
"processed_items": [item.upper() for item in input_data.get("items", [])],
|
||||
"timestamp": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
```
|
||||
|
||||
The `result` dict becomes `window.OUTPUT_BACKEND_RESULT` in the frontend.
|
||||
|
||||
---
|
||||
|
||||
## Multi-file projects
|
||||
|
||||
Split code across files for organization. All files are served from the
|
||||
workspace root, so relative imports work naturally:
|
||||
|
||||
```
|
||||
workspace/
|
||||
├── index.html
|
||||
├── meta.json
|
||||
├── schema.json
|
||||
├── styles/
|
||||
│ └── main.css
|
||||
├── components/
|
||||
│ └── Chart.js
|
||||
└── utils/
|
||||
└── helpers.js
|
||||
```
|
||||
|
||||
Reference from `index.html`:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="./styles/main.css">
|
||||
<script type="module" src="./components/Chart.js"></script>
|
||||
```
|
||||
|
||||
ES module imports between JS files:
|
||||
|
||||
```javascript
|
||||
// components/Chart.js
|
||||
import { formatNumber } from '../utils/helpers.js';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using React
|
||||
|
||||
React 18 is available via esm.sh CDN — no build step needed:
|
||||
|
||||
```html
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@18",
|
||||
"react-dom/client": "https://esm.sh/react-dom@18/client"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<script type="module">
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function App() {
|
||||
const input = window.OUTPUT_INPUT || {};
|
||||
return React.createElement('div', null,
|
||||
React.createElement('h1', null, input.title || 'Hello')
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
React.createElement(App)
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
Other CDN libraries work too — use `https://esm.sh/` or `https://cdn.jsdelivr.net/npm/` for any npm package.
|
||||
|
||||
---
|
||||
|
||||
## Design guidelines
|
||||
|
||||
- **Dark theme by default** — use dark backgrounds (#0f1117, #1a1d27) with
|
||||
light text (#e2e8f0) unless the user requests otherwise.
|
||||
- **Modern aesthetics** — rounded corners (8-12px), subtle borders, box shadows,
|
||||
smooth transitions (0.15-0.3s ease).
|
||||
- **Responsive** — use flexbox/grid, test at different sizes.
|
||||
- **Typography** — system font stack for UI, monospace for code/data.
|
||||
- **Color accents** — use a single accent color with variations for hover/active states.
|
||||
- **Spacing** — consistent padding (12-20px), adequate whitespace between sections.
|
||||
- **Interactivity** — hover effects, focus states, loading indicators where appropriate.
|
||||
|
||||
---
|
||||
|
||||
## Complete minimal example
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My App</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #0f1117;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card {
|
||||
background: #1a1d27;
|
||||
border: 1px solid #2e3248;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
h1 { font-size: 1.5rem; margin-bottom: 8px; }
|
||||
p { color: #8892a4; line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1 id="title">Loading…</h1>
|
||||
<p id="desc"></p>
|
||||
</div>
|
||||
<script>
|
||||
const input = window.OUTPUT_INPUT || {};
|
||||
document.getElementById('title').textContent = input.title || 'Untitled';
|
||||
document.getElementById('desc').textContent = input.description || 'No description provided.';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Default template files seeded into new App Builder workspaces."""
|
||||
|
||||
import os
|
||||
|
||||
_SKILL_PATH = os.path.join(os.path.dirname(__file__), "view_builder_skill.md")
|
||||
|
||||
with open(_SKILL_PATH) as _f:
|
||||
VIEW_BUILDER_SKILL = _f.read()
|
||||
|
||||
VIEW_TEMPLATE_INDEX = """\
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>App</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f1117;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.container {
|
||||
background: #1a1d27;
|
||||
border: 1px solid #2e3248;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
|
||||
p { color: #8892a4; font-size: 0.95rem; line-height: 1.6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 id="title">Ready</h1>
|
||||
<p id="desc">Describe what you want to build and the agent will update this app.</p>
|
||||
</div>
|
||||
<script>
|
||||
const input = window.OUTPUT_INPUT || {};
|
||||
const result = window.OUTPUT_BACKEND_RESULT || null;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
VIEW_TEMPLATE_SCHEMA = """\
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
"""
|
||||
|
||||
VIEW_TEMPLATE_META = """\
|
||||
{
|
||||
"name": "",
|
||||
"description": ""
|
||||
}
|
||||
"""
|
||||
|
||||
VIEW_TEMPLATE_FILES = {
|
||||
"index.html": VIEW_TEMPLATE_INDEX,
|
||||
"schema.json": VIEW_TEMPLATE_SCHEMA,
|
||||
"meta.json": VIEW_TEMPLATE_META,
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openswarm",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openswarm",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openswarm",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.10",
|
||||
"description": "OpenSwarm — AI Agent Orchestrator",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -404,11 +404,6 @@ const AgentCard: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollapse = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dispatch(collapseSession(session.id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (session.status === 'running' || session.status === 'waiting_approval') {
|
||||
@@ -750,37 +745,20 @@ const AgentCard: React.FC<Props> = ({
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0, ml: 0.5 }}
|
||||
>
|
||||
{expanded ? (
|
||||
<Tooltip title="Collapse">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleCollapse}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
color: c.text.ghost,
|
||||
p: 0.5,
|
||||
'&:hover': { color: c.text.secondary, bgcolor: c.bg.secondary },
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={isDraft ? 'Remove' : 'Close chat'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRemove}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
color: c.text.ghost,
|
||||
p: 0.5,
|
||||
'&:hover': { color: c.status.error, bgcolor: `${c.status.errorBg}` },
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={isDraft ? 'Remove' : 'Close chat'}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleRemove}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
color: c.text.ghost,
|
||||
p: 0.5,
|
||||
'&:hover': { color: c.status.error, bgcolor: `${c.status.errorBg}` },
|
||||
}}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import RestoreIcon from '@mui/icons-material/Restore';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
|
||||
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
createMode,
|
||||
updateMode,
|
||||
deleteMode,
|
||||
resetMode,
|
||||
Mode,
|
||||
} from '@/shared/state/modesSlice';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
@@ -105,7 +107,7 @@ const ALL_BUILTIN_TOOL_NAMES = ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep',
|
||||
const Modes: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const { items, loading } = useAppSelector((s) => s.modes);
|
||||
const { items, builtinDefaults, loading } = useAppSelector((s) => s.modes);
|
||||
const toolItems = useAppSelector((s) => s.tools.items);
|
||||
const modes = useMemo(() => Object.values(items), [items]);
|
||||
|
||||
@@ -174,6 +176,45 @@ const Modes: React.FC = () => {
|
||||
await dispatch(deleteMode(id));
|
||||
};
|
||||
|
||||
const editingIsBuiltin = editingId ? items[editingId]?.is_builtin ?? false : false;
|
||||
|
||||
const hasDiverged = useMemo(() => {
|
||||
if (!editingId || !editingIsBuiltin) return false;
|
||||
const defaults = builtinDefaults[editingId];
|
||||
if (!defaults) return false;
|
||||
const current = items[editingId];
|
||||
if (!current) return false;
|
||||
return (
|
||||
current.name !== defaults.name ||
|
||||
current.description !== defaults.description ||
|
||||
(current.system_prompt ?? '') !== (defaults.system_prompt ?? '') ||
|
||||
JSON.stringify(current.tools) !== JSON.stringify(defaults.tools) ||
|
||||
(current.default_next_mode ?? '') !== (defaults.default_next_mode ?? '') ||
|
||||
current.icon !== defaults.icon ||
|
||||
current.color !== defaults.color ||
|
||||
(current.default_folder ?? '') !== (defaults.default_folder ?? '')
|
||||
);
|
||||
}, [editingId, editingIsBuiltin, items, builtinDefaults]);
|
||||
|
||||
const handleReset = async () => {
|
||||
if (!editingId) return;
|
||||
const action = await dispatch(resetMode(editingId));
|
||||
if (resetMode.fulfilled.match(action)) {
|
||||
const m = action.payload;
|
||||
setForm({
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
system_prompt: m.system_prompt ?? '',
|
||||
tools: m.tools ?? [],
|
||||
toolsEnabled: m.tools !== null,
|
||||
default_next_mode: m.default_next_mode ?? '',
|
||||
icon: m.icon,
|
||||
color: m.color,
|
||||
default_folder: m.default_folder ?? '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const otherModes = modes.filter((m) => m.id !== editingId);
|
||||
|
||||
return (
|
||||
@@ -505,23 +546,46 @@ const Modes: React.FC = () => {
|
||||
</FormControl>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setDialogOpen(false)} sx={{ color: c.text.tertiary, textTransform: 'none' }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={!form.name}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{editingId ? 'Save Changes' : 'Create Mode'}
|
||||
</Button>
|
||||
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
{editingIsBuiltin && (
|
||||
<Tooltip title={hasDiverged ? 'Restore this mode to its original built-in defaults' : 'Mode matches built-in defaults'}>
|
||||
<span>
|
||||
<Button
|
||||
startIcon={<RestoreIcon sx={{ fontSize: 16 }} />}
|
||||
onClick={handleReset}
|
||||
disabled={!hasDiverged}
|
||||
sx={{
|
||||
color: hasDiverged ? c.text.muted : c.text.ghost,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.82rem',
|
||||
'&:hover': hasDiverged ? { color: c.status.error, bgcolor: `${c.status.error}10` } : {},
|
||||
}}
|
||||
>
|
||||
Reset to Default
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button onClick={() => setDialogOpen(false)} sx={{ color: c.text.tertiary, textTransform: 'none' }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
disabled={!form.name}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{editingId ? 'Save Changes' : 'Create Mode'}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
|
||||
@@ -381,7 +381,7 @@ interface FileTreeItemProps {
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
}
|
||||
|
||||
const PROTECTED_FILES = new Set(['index.html', 'schema.json', 'meta.json']);
|
||||
const PROTECTED_FILES = new Set(['index.html', 'schema.json', 'meta.json', 'SKILL.md']);
|
||||
|
||||
const FileTreeItem: React.FC<FileTreeItemProps> = ({ node, depth, activeFile, onSelect, onDelete, c }) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
@@ -732,6 +732,7 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
const outputFiles = { ...files };
|
||||
delete outputFiles['meta.json'];
|
||||
delete outputFiles['schema.json'];
|
||||
delete outputFiles['SKILL.md'];
|
||||
|
||||
return {
|
||||
name: name || 'Untitled App',
|
||||
@@ -948,7 +949,7 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
? `${SERVE_BASE}/workspace/${workspaceId}/serve/index.html`
|
||||
: undefined;
|
||||
|
||||
const filePaths = useMemo(() => Object.keys(files).filter(p => p !== 'meta.json').sort(), [files]);
|
||||
const filePaths = useMemo(() => Object.keys(files).filter(p => p !== 'meta.json' && p !== 'SKILL.md').sort(), [files]);
|
||||
const fileTree = useMemo(() => buildFileTree(filePaths), [filePaths]);
|
||||
|
||||
const updateFile = useCallback((path: string, content: string) => {
|
||||
|
||||
@@ -18,18 +18,19 @@ export interface Mode {
|
||||
|
||||
interface ModesState {
|
||||
items: Record<string, Mode>;
|
||||
builtinDefaults: Record<string, Mode>;
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const initialState: ModesState = { items: {}, loading: false, loaded: false };
|
||||
const initialState: ModesState = { items: {}, builtinDefaults: {}, loading: false, loaded: false };
|
||||
|
||||
export const fetchModes = createAsyncThunk(
|
||||
'modes/fetch',
|
||||
async () => {
|
||||
const res = await fetch(`${MODES_API}/list`);
|
||||
const data = await res.json();
|
||||
return data.modes as Mode[];
|
||||
return { modes: data.modes as Mode[], builtinDefaults: (data.builtin_defaults ?? {}) as Record<string, Mode> };
|
||||
},
|
||||
{ condition: (_, { getState }) => !(getState() as { modes: ModesState }).modes.loading },
|
||||
);
|
||||
@@ -60,6 +61,15 @@ export const updateMode = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const resetMode = createAsyncThunk(
|
||||
'modes/reset',
|
||||
async (id: string) => {
|
||||
const res = await fetch(`${MODES_API}/${id}/reset`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
return data.mode as Mode;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteMode = createAsyncThunk('modes/delete', async (id: string) => {
|
||||
await fetch(`${MODES_API}/${id}`, { method: 'DELETE' });
|
||||
return id;
|
||||
@@ -76,11 +86,13 @@ const modesSlice = createSlice({
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
state.items = {};
|
||||
for (const m of action.payload) state.items[m.id] = m;
|
||||
for (const m of action.payload.modes) state.items[m.id] = m;
|
||||
state.builtinDefaults = action.payload.builtinDefaults;
|
||||
})
|
||||
.addCase(fetchModes.rejected, (state) => { state.loading = false; state.loaded = true; })
|
||||
.addCase(createMode.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(updateMode.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(resetMode.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(deleteMode.fulfilled, (state, action) => { delete state.items[action.payload]; });
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user