From 58e10a5a95a52f6affb531d399d8a846572fe0a6 Mon Sep 17 00:00:00 2001 From: Thomas Marchand Date: Fri, 16 Jan 2026 16:48:52 +0000 Subject: [PATCH] Add library item rename with cascading reference updates - Add POST /api/library/rename/:item_type/:name endpoint supporting skills, commands, rules, agents, tools, and workspace templates - Implement dry_run mode to preview changes before applying - Auto-update all cross-references in related configs and workspaces - Add RenameDialog component with preview and apply workflow - Integrate rename action into Skills, Commands, and Rules config pages - Fix settings page to sync config before restarting OpenCode - Clarify INSTALL.md dashboard deployment options (Vercel vs local) - Add docs-site scaffolding (Nextra-based documentation) --- INSTALL.md | 34 +- dashboard/src/app/config/commands/page.tsx | 28 + dashboard/src/app/config/rules/page.tsx | 27 + dashboard/src/app/config/settings/page.tsx | 145 ++- dashboard/src/app/config/skills/page.tsx | 48 +- dashboard/src/components/rename-dialog.tsx | 291 +++++ dashboard/src/components/sidebar.tsx | 4 +- dashboard/src/lib/api.ts | 53 + docs-site/.gitignore | 31 + docs-site/app/[[...mdxPath]]/docs.css | 919 ++++++++++++++ docs-site/app/[[...mdxPath]]/layout.tsx | 50 + docs-site/app/[[...mdxPath]]/page.tsx | 39 + docs-site/app/api/docs/[...slug]/route.ts | 219 ++++ docs-site/app/globals.css | 481 +++++++ docs-site/app/layout.tsx | 108 ++ docs-site/bun.lock | 1127 +++++++++++++++++ docs-site/components/theme-provider.tsx | 16 + docs-site/content/_meta.js | 17 + docs-site/content/api.mdx | 363 ++++++ docs-site/content/desktop.mdx | 328 +++++ docs-site/content/first-mission.mdx | 111 ++ docs-site/content/index.mdx | 100 ++ docs-site/content/library.mdx | 204 +++ docs-site/content/setup.mdx | 175 +++ docs-site/content/workspaces.mdx | 217 ++++ docs-site/mdx-components.tsx | 77 ++ docs-site/next.config.mjs | 19 + docs-site/package.json | 32 + docs-site/postcss.config.mjs | 5 + docs-site/proxy.ts | 99 ++ docs-site/public/apple-touch-icon.png | Bin 0 -> 47077 bytes docs-site/public/favicon.svg | 10 + docs-site/public/fonts/.gitkeep | 1 + .../public/fonts/Geist-VariableFont_wght.ttf | Bin 0 -> 148768 bytes .../fonts/GeistMono-VariableFont_wght.ttf | Bin 0 -> 137740 bytes docs-site/public/llms.txt | 87 ++ docs-site/public/og-image.png | Bin 0 -> 60065 bytes docs-site/public/robots.txt | 9 + docs-site/tsconfig.json | 34 + src/api/library.rs | 173 +++ src/library/mod.rs | 1 + src/library/rename.rs | 470 +++++++ 42 files changed, 6076 insertions(+), 76 deletions(-) create mode 100644 dashboard/src/components/rename-dialog.tsx create mode 100644 docs-site/.gitignore create mode 100644 docs-site/app/[[...mdxPath]]/docs.css create mode 100644 docs-site/app/[[...mdxPath]]/layout.tsx create mode 100644 docs-site/app/[[...mdxPath]]/page.tsx create mode 100644 docs-site/app/api/docs/[...slug]/route.ts create mode 100644 docs-site/app/globals.css create mode 100644 docs-site/app/layout.tsx create mode 100644 docs-site/bun.lock create mode 100644 docs-site/components/theme-provider.tsx create mode 100644 docs-site/content/_meta.js create mode 100644 docs-site/content/api.mdx create mode 100644 docs-site/content/desktop.mdx create mode 100644 docs-site/content/first-mission.mdx create mode 100644 docs-site/content/index.mdx create mode 100644 docs-site/content/library.mdx create mode 100644 docs-site/content/setup.mdx create mode 100644 docs-site/content/workspaces.mdx create mode 100644 docs-site/mdx-components.tsx create mode 100644 docs-site/next.config.mjs create mode 100644 docs-site/package.json create mode 100644 docs-site/postcss.config.mjs create mode 100644 docs-site/proxy.ts create mode 100644 docs-site/public/apple-touch-icon.png create mode 100644 docs-site/public/favicon.svg create mode 100644 docs-site/public/fonts/.gitkeep create mode 100644 docs-site/public/fonts/Geist-VariableFont_wght.ttf create mode 100644 docs-site/public/fonts/GeistMono-VariableFont_wght.ttf create mode 100644 docs-site/public/llms.txt create mode 100644 docs-site/public/og-image.png create mode 100644 docs-site/public/robots.txt create mode 100644 docs-site/tsconfig.json create mode 100644 src/library/rename.rs diff --git a/INSTALL.md b/INSTALL.md index 8b6f306..364a705 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -767,20 +767,40 @@ Note: Multi-user mode provides separate login credentials but does **not** provi ## 12) Dashboard Configuration -### 12.1 Web Dashboard +This guide installs the **backend** on your server. The dashboard (frontend) is separate and you have several options: -The web dashboard auto-detects the backend URL. For production, set the environment variable: +| Option | Best For | Setup | +|--------|----------|-------| +| **Vercel** | Production, always accessible | Deploy `dashboard/` to Vercel | +| **Local** | Development, quick testing | Run `bun dev` in dashboard folder | +| **iOS App** | Mobile access | Enter backend URL in app | + +### 12.1 Web Dashboard (Vercel) + +Deploy the `dashboard/` folder to [Vercel](https://vercel.com): + +1. Connect your repo to Vercel +2. Set the root directory to `dashboard` +3. Add environment variable: `NEXT_PUBLIC_API_URL=https://agent.yourdomain.com` +4. Deploy + +The dashboard will connect to your backend server. + +### 12.2 Web Dashboard (Local) + +Run the dashboard locally on your machine: ```bash -# When building/running the Next.js dashboard -NEXT_PUBLIC_API_URL=https://agent.yourdomain.com +cd dashboard +bun install +NEXT_PUBLIC_API_URL=https://agent.yourdomain.com bun dev ``` -Or configure it at runtime via the Settings page. +Then open `http://localhost:3000`. -### 12.2 iOS App +### 12.3 iOS App -On first launch, the iOS app prompts for the server URL. Enter your production URL (e.g., `https://agent.yourdomain.com`). +On first launch, the iOS app prompts for the server URL. Enter your backend URL (e.g., `https://agent.yourdomain.com`). To change later: **Menu (⋮) → Settings** diff --git a/dashboard/src/app/config/commands/page.tsx b/dashboard/src/app/config/commands/page.tsx index 79753cc..f890cee 100644 --- a/dashboard/src/app/config/commands/page.tsx +++ b/dashboard/src/app/config/commands/page.tsx @@ -16,11 +16,13 @@ import { Save, Trash2, FileText, + Pencil, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { LibraryUnavailable } from '@/components/library-unavailable'; import { useLibrary } from '@/contexts/library-context'; import { ConfigCodeEditor } from '@/components/config-code-editor'; +import { RenameDialog } from '@/components/rename-dialog'; export default function CommandsPage() { const { @@ -46,6 +48,7 @@ export default function CommandsPage() { const [commandSaving, setCommandSaving] = useState(false); const [loadingCommand, setLoadingCommand] = useState(false); const [showNewCommandDialog, setShowNewCommandDialog] = useState(false); + const [showRenameDialog, setShowRenameDialog] = useState(false); const [newCommandName, setNewCommandName] = useState(''); const [commitMessage, setCommitMessage] = useState(''); const [showCommitDialog, setShowCommitDialog] = useState(false); @@ -159,6 +162,12 @@ Describe what this command does. } }; + const handleRenameSuccess = async () => { + await refresh(); + setSelectedCommand(null); + setCommandContent(''); + }; + if (loading) { return (
@@ -304,9 +313,17 @@ Describe what this command does.
{commandDirty && Unsaved} + @@ -416,6 +433,17 @@ Describe what this command does.
)} + + {/* Rename Dialog */} + {selectedCommand && ( + + )} ); } diff --git a/dashboard/src/app/config/rules/page.tsx b/dashboard/src/app/config/rules/page.tsx index 661d4d4..b028484 100644 --- a/dashboard/src/app/config/rules/page.tsx +++ b/dashboard/src/app/config/rules/page.tsx @@ -14,11 +14,13 @@ import { Trash2, X, FileText, + Pencil, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { LibraryUnavailable } from '@/components/library-unavailable'; import { useLibrary } from '@/contexts/library-context'; import { ConfigCodeEditor } from '@/components/config-code-editor'; +import { RenameDialog } from '@/components/rename-dialog'; export default function RulesPage() { const { @@ -44,6 +46,7 @@ export default function RulesPage() { const [ruleSaving, setRuleSaving] = useState(false); const [loadingRule, setLoadingRule] = useState(false); const [showNewRuleDialog, setShowNewRuleDialog] = useState(false); + const [showRenameDialog, setShowRenameDialog] = useState(false); const [newRuleName, setNewRuleName] = useState(''); const [commitMessage, setCommitMessage] = useState(''); const [showCommitDialog, setShowCommitDialog] = useState(false); @@ -178,6 +181,12 @@ Describe what this rule does. } }; + const handleRenameSuccess = async () => { + await refresh(); + setSelectedRule(null); + setRuleContent(''); + }; + if (loading) { return (
@@ -324,6 +333,13 @@ Describe what this rule does.
{ruleDirty && Unsaved} +
)} + + {/* Rename Dialog */} + {selectedRule && ( + + )} ); } diff --git a/dashboard/src/app/config/settings/page.tsx b/dashboard/src/app/config/settings/page.tsx index 3508c8d..20aec81 100644 --- a/dashboard/src/app/config/settings/page.tsx +++ b/dashboard/src/app/config/settings/page.tsx @@ -201,6 +201,8 @@ export default function SettingsPage() { try { setRestarting(true); setError(null); + // Sync config before restarting + await sync(); await restartOpenCodeService(); setRestartSuccess(true); setNeedsRestart(false); @@ -328,47 +330,6 @@ export default function SettingsPage() { )} - {/* Header */} -
-
-

Configs

-

- Configure OpenCode and OpenAgent settings -

-
-
- - -
-
- {/* Error Display */} {error && (
@@ -481,6 +442,37 @@ export default function SettingsPage() { )} {saving ? 'Saving...' : saveSuccess ? 'Saved!' : 'Save'} + +
@@ -520,25 +512,58 @@ export default function SettingsPage() {

OpenAgent Settings

Configure agent visibility in mission dialog

- +
+ + + +
{/* Agent Visibility */} diff --git a/dashboard/src/app/config/skills/page.tsx b/dashboard/src/app/config/skills/page.tsx index 8cb4e04..8d98f87 100644 --- a/dashboard/src/app/config/skills/page.tsx +++ b/dashboard/src/app/config/skills/page.tsx @@ -29,11 +29,13 @@ import { Download, FileText, ExternalLink, + Pencil, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { LibraryUnavailable } from '@/components/library-unavailable'; import { useLibrary } from '@/contexts/library-context'; import { ConfigCodeEditor } from '@/components/config-code-editor'; +import { RenameDialog } from '@/components/rename-dialog'; // ───────────────────────────────────────────────────────────────────────────── // Types @@ -640,6 +642,7 @@ export default function SkillsPage() { const [showImportDialog, setShowImportDialog] = useState(false); const [showNewFileDialog, setShowNewFileDialog] = useState(false); const [showCommitDialog, setShowCommitDialog] = useState(false); + const [showRenameDialog, setShowRenameDialog] = useState(false); const [newSkillName, setNewSkillName] = useState(''); const [newSkillError, setNewSkillError] = useState(null); const [commitMessage, setCommitMessage] = useState(''); @@ -921,6 +924,17 @@ Describe what this skill does. await loadSkill(skill.name); }; + const handleRenameSuccess = async () => { + await refresh(); + // The skill was renamed, so we need to clear selection + // since the old name no longer exists + setSelectedSkill(null); + setSelectedFile(null); + setFileContent(''); + setFrontmatter({}); + setBodyContent(''); + }; + if (loading) { return (
@@ -1104,13 +1118,22 @@ Describe what this skill does.
{isDirty && Unsaved} {selectedFile === 'SKILL.md' && ( - + <> + + + )}
); } diff --git a/dashboard/src/components/rename-dialog.tsx b/dashboard/src/components/rename-dialog.tsx new file mode 100644 index 0000000..50f5a1a --- /dev/null +++ b/dashboard/src/components/rename-dialog.tsx @@ -0,0 +1,291 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { Loader, AlertCircle, FileEdit, ArrowRight, X } from "lucide-react"; +import { + renameLibraryItem, + LibraryItemType, + RenameResult, + RenameChange, +} from "@/lib/api"; +import { cn } from "@/lib/utils"; + +interface RenameDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + itemType: LibraryItemType; + currentName: string; + onSuccess?: () => void; +} + +const ITEM_TYPE_LABELS: Record = { + skill: "Skill", + command: "Command", + rule: "Rule", + agent: "Agent", + tool: "Tool", + "workspace-template": "Workspace Template", +}; + +function ChangePreview({ changes }: { changes: RenameChange[] }) { + if (changes.length === 0) return null; + + return ( +
+ +
+ {changes.map((change, i) => ( +
+ {change.type === "rename_file" && ( + <> + + {change.from} + + {change.to} + + )} + {change.type === "update_reference" && ( + <> + + {change.file} + ({change.field}) + + )} + {change.type === "update_workspace" && ( + <> + + + Workspace: {change.workspace_name} + + ({change.field}) + + )} +
+ ))} +
+
+ ); +} + +export function RenameDialog({ + open, + onOpenChange, + itemType, + currentName, + onSuccess, +}: RenameDialogProps) { + const [newName, setNewName] = useState(currentName); + const [loading, setLoading] = useState(false); + const [previewing, setPreviewing] = useState(false); + const [preview, setPreview] = useState(null); + const [error, setError] = useState(null); + + const label = ITEM_TYPE_LABELS[itemType]; + + // Reset state when dialog opens/closes + useEffect(() => { + if (open) { + setNewName(currentName); + setPreview(null); + setError(null); + } + }, [open, currentName]); + + // Handle escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && open) { + onOpenChange(false); + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [open, onOpenChange]); + + const handlePreview = useCallback(async () => { + if (!newName.trim() || newName === currentName) return; + + setPreviewing(true); + setError(null); + setPreview(null); + + try { + const result = await renameLibraryItem( + itemType, + currentName, + newName.trim(), + true // dry_run + ); + setPreview(result); + if (!result.success) { + setError(result.error || "Preview failed"); + } + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to preview rename"); + } finally { + setPreviewing(false); + } + }, [itemType, currentName, newName]); + + const handleRename = useCallback(async () => { + if (!newName.trim() || newName === currentName) return; + + setLoading(true); + setError(null); + + try { + const result = await renameLibraryItem( + itemType, + currentName, + newName.trim(), + false // execute + ); + + if (result.success) { + onOpenChange(false); + onSuccess?.(); + } else { + setError(result.error || "Rename failed"); + } + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to rename"); + } finally { + setLoading(false); + } + }, [itemType, currentName, newName, onOpenChange, onSuccess]); + + const isValid = newName.trim() && newName !== currentName; + + if (!open) return null; + + return ( +
+
+
+

Rename {label}

+ +
+ +

+ Enter a new name for this {label.toLowerCase()}. All references will + be automatically updated. +

+ +
+
+ + +
+ +
+ + { + setNewName(e.target.value); + setPreview(null); + setError(null); + }} + placeholder={`Enter new ${label.toLowerCase()} name`} + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter" && isValid && !loading && !previewing) { + if (preview?.success) { + handleRename(); + } else { + handlePreview(); + } + } + }} + className="w-full px-4 py-2 rounded-lg bg-white/[0.04] border border-white/[0.08] text-white placeholder:text-white/30 focus:outline-none focus:border-indigo-500/50" + /> +
+ + {error && ( +
+ + {error} +
+ )} + + {preview?.success && } + + {preview?.warnings && preview.warnings.length > 0 && ( +
+ + {preview.warnings.join(", ")} +
+ )} +
+ +
+ + + {!preview?.success ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/dashboard/src/components/sidebar.tsx b/dashboard/src/components/sidebar.tsx index 7418dd1..b3e77af 100644 --- a/dashboard/src/components/sidebar.tsx +++ b/dashboard/src/components/sidebar.tsx @@ -139,8 +139,8 @@ export function Sidebar() {
- {/* Navigation */} -