diff --git a/.env.example b/.env.example index b6e3a863..fc6e1edc 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,30 @@ PORT=8080 SECRETS_ENCRYPTION_KEY= # 32-byte key (raw or base64). Leave empty for plaintext (dev only). CONFIGS_DIR= # Path to workspace-configs-templates/ (auto-discovered if empty) PLUGINS_DIR= # Path to plugins/ directory (default: /plugins in container) +# PLATFORM_URL=http://host.docker.internal:8080 # URL agent containers use to reach the platform; injected into workspace env. Default derives from PORT. +# MOLECULE_URL=http://localhost:8080 # Canonical MCP-client URL (mirrors PLATFORM_URL inside containers). Read by the MCP server (mcp-server/) and Molecule MCP tooling. +# WORKSPACE_DIR= # Optional global host path bind-mounted to /workspace in every container. Per-workspace workspace_dir column overrides this; if neither is set each workspace gets an isolated Docker named volume. +# MOLECULE_ENV=development # Environment label (development/staging/production). Used for log tagging and conditional behaviour. + +# CORS / rate limiting +# CORS_ORIGINS=http://localhost:3000,http://localhost:3001 # Comma-separated allowed origins for the HTTP API. +# RATE_LIMIT=600 # Requests/minute per client (default 600). + +# Activity retention +# ACTIVITY_RETENTION_DAYS=7 # Days to keep rows in activity_logs before pruning. +# ACTIVITY_CLEANUP_INTERVAL_HOURS=6 # How often the background pruner runs. + +# Container/runtime detection +# MOLECULE_IN_DOCKER= # Set when running the platform inside Docker (accepts 1/0, true/false). Triggers A2A proxy to rewrite 127.0.0.1: agent URLs to Docker bridge hostnames. Auto-detected via /.dockerenv; only set if detection fails or to force off. + +# Observability (Awareness) +# AWARENESS_URL= # If set, injected into workspace containers along with a deterministic AWARENESS_NAMESPACE derived from workspace ID. Enables the cross-session memory MCP server. + +# Webhooks +# GITHUB_WEBHOOK_SECRET= # HMAC secret used to verify incoming GitHub webhook payloads at /webhooks/github. + +# CLI clients +# MOLECLI_URL=http://localhost:8080 # URL the molecli TUI uses to reach the platform. # Plugin install safeguards (POST /workspaces/:id/plugins) # All three bound the cost of a single install so a slow/malicious @@ -39,7 +63,6 @@ CEREBRAS_API_KEY= # Cerebras API key (cloud.cerebras.ai). Use with GOOGLE_API_KEY= # Google AI API key (aistudio.google.com). Use with model: google_genai:gemini-2.5-flash MAX_TOKENS=2048 # Max output tokens for OpenRouter requests (default: 2048) LANGGRAPH_RECURSION_LIMIT=500 # LangGraph/DeepAgents max ReAct steps per turn (lib default: 25; raised to 500 — PM fan-out to 6+ reports + synthesis routinely exceeds 100) -MOLECULE_IN_DOCKER= # Set when running the platform inside Docker (accepts 1/0, true/false — anything strconv.ParseBool recognises). Triggers A2A proxy to rewrite 127.0.0.1: agent URLs to Docker bridge hostnames. Auto-detected via /.dockerenv; only set if detection fails (e.g. Podman, custom runtimes) or to force off. MODEL_PROVIDER=anthropic:claude-sonnet-4-6 # Format: provider:model. Providers: anthropic, openai, openrouter, groq, cerebras, google_genai, ollama # Social Channels (optional — configure per-workspace via API or Canvas) diff --git a/canvas/src/components/ErrorBoundary.tsx b/canvas/src/components/ErrorBoundary.tsx index 495da276..60135f73 100644 --- a/canvas/src/components/ErrorBoundary.tsx +++ b/canvas/src/components/ErrorBoundary.tsx @@ -41,11 +41,10 @@ export class ErrorBoundary extends React.Component< }; // Log the full report to console for collection by monitoring tools console.error("Error Report:", JSON.stringify(errorDetails, null, 2)); - // Copy error info to clipboard for manual reporting - navigator.clipboard?.writeText(JSON.stringify(errorDetails, null, 2)).then( - () => window.alert("Error details copied to clipboard."), - () => window.alert("Error details logged to console.") - ); + // Copy error info to clipboard for manual reporting (button click is its + // own affordance — no native alert needed). On clipboard failure the + // console.error above still surfaces the report. + navigator.clipboard?.writeText(JSON.stringify(errorDetails, null, 2)); }; render() { diff --git a/canvas/src/components/TemplatePalette.tsx b/canvas/src/components/TemplatePalette.tsx index ba821834..ed6c7ec7 100644 --- a/canvas/src/components/TemplatePalette.tsx +++ b/canvas/src/components/TemplatePalette.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { api } from "@/lib/api"; import { checkDeploySecrets, type PreflightResult } from "@/lib/deploy-preflight"; import { MissingKeysModal } from "./MissingKeysModal"; +import { ConfirmDialog } from "./ConfirmDialog"; interface Template { id: string; @@ -144,6 +145,7 @@ const TIER_LABELS: Record = { function ImportAgentButton({ onImported }: { onImported: () => void }) { const [importing, setImporting] = useState(false); + const [notice, setNotice] = useState(null); const fileInputRef = useRef(null); const handleFiles = async (fileList: FileList) => { @@ -173,7 +175,7 @@ function ImportAgentButton({ onImported }: { onImported: () => void }) { } if (Object.keys(files).length === 0) { - alert("No files found in the selected folder"); + setNotice("No files found in the selected folder"); return; } @@ -181,7 +183,7 @@ function ImportAgentButton({ onImported }: { onImported: () => void }) { await api.post("/templates/import", { name, files }); onImported(); } catch (e) { - alert(e instanceof Error ? e.message : "Import failed"); + setNotice(e instanceof Error ? e.message : "Import failed"); } finally { setImporting(false); } @@ -205,6 +207,15 @@ function ImportAgentButton({ onImported }: { onImported: () => void }) { > {importing ? "Importing..." : "Import Agent Folder"} + setNotice(null)} + onCancel={() => setNotice(null)} + /> ); } diff --git a/canvas/src/components/tabs/ChannelsTab.tsx b/canvas/src/components/tabs/ChannelsTab.tsx index 68ddda4e..de22810b 100644 --- a/canvas/src/components/tabs/ChannelsTab.tsx +++ b/canvas/src/components/tabs/ChannelsTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { api } from "@/lib/api"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; interface ChannelAdapter { type: string; @@ -39,6 +40,7 @@ export function ChannelsTab({ workspaceId }: Props) { const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); const [testing, setTesting] = useState(null); + const [pendingDelete, setPendingDelete] = useState(null); // Form state const [formType, setFormType] = useState("telegram"); @@ -146,8 +148,10 @@ export function ChannelsTab({ workspaceId }: Props) { load(); }; - const handleDelete = async (ch: Channel) => { - if (!confirm(`Delete ${ch.channel_type} channel?`)) return; + const confirmDelete = async () => { + if (!pendingDelete) return; + const ch = pendingDelete; + setPendingDelete(null); await api.del(`/workspaces/${workspaceId}/channels/${ch.id}`); load(); }; @@ -338,7 +342,7 @@ export function ChannelsTab({ workspaceId }: Props) { {ch.enabled ? "On" : "Off"} + + { + useCanvasStore.getState().restartWorkspace(workspaceId); + setConfirmRestart(false); + }} + onCancel={() => setConfirmRestart(false)} + /> ); } diff --git a/canvas/src/components/tabs/ScheduleTab.tsx b/canvas/src/components/tabs/ScheduleTab.tsx index 61155267..0dc4e9bd 100644 --- a/canvas/src/components/tabs/ScheduleTab.tsx +++ b/canvas/src/components/tabs/ScheduleTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { api } from "@/lib/api"; +import { ConfirmDialog } from "@/components/ConfirmDialog"; interface Schedule { id: string; @@ -64,6 +65,7 @@ export function ScheduleTab({ workspaceId }: Props) { const [formPrompt, setFormPrompt] = useState(""); const [formEnabled, setFormEnabled] = useState(true); const [error, setError] = useState(""); + const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null); const fetchSchedules = useCallback(async () => { try { @@ -120,8 +122,10 @@ export function ScheduleTab({ workspaceId }: Props) { } }; - const handleDelete = async (id: string, name: string) => { - if (!window.confirm(`Delete schedule "${name || "Unnamed"}"? This cannot be undone.`)) return; + const confirmDelete = async () => { + if (!pendingDelete) return; + const { id } = pendingDelete; + setPendingDelete(null); await api.del(`/workspaces/${workspaceId}/schedules/${id}`); fetchSchedules(); }; @@ -343,7 +347,7 @@ export function ScheduleTab({ workspaceId }: Props) { ✎