fix: update custom agent handling and improve agent loading
- Update `wrangler.jsonc` to correct the `CUSTOM_AGENT_WEBHOOK` URL. - Enhance `page.tsx` to load custom agents from localStorage and merge them with predefined agents. - Modify `route.ts` to validate `systemPrompt` for custom agents and include it in the webhook payload. - Adjust `chat-interface.tsx` to handle custom agents more effectively, including system prompt integration and event dispatching for pinned agents. - Remove obsolete `CUSTOM_AGENT_EXECUTION_PRD.md` and `DIFF_TOOL_USAGE.md` files as part of cleanup.
This commit is contained in:
parent
ee254fc5c3
commit
99613f9ad4
7
.cursor/commands/commit-changes.md
Normal file
7
.cursor/commands/commit-changes.md
Normal file
@ -0,0 +1,7 @@
|
||||
Commit your changes:
|
||||
|
||||
1. Stage prep: Review git status to understand dirty files, then inspect diffs per file (git diff path/to/file) to confirm only the intended changes go in.
|
||||
2. Quality gates: Run pnpm lint, relevant tests (pnpm test, or targeted suites), and—if UI work—manually verify in browser per UI Development Protocol.
|
||||
3. Summaries: Update docs or comments if behavior changed; keep CHANGELOG or task ticket notes in sync if you use them.
|
||||
4. Commit message: Compose conventional, task-oriented messages (e.g., feat: add pinned agent reordering animation) before committing with git commit -am "... or git commit after staging via git add.
|
||||
5. Final check: Re-run git status to confirm a clean tree.
|
||||
11
.cursor/commands/deploy-prod.md
Normal file
11
.cursor/commands/deploy-prod.md
Normal file
@ -0,0 +1,11 @@
|
||||
Deploy the application to cloudflare workers.
|
||||
|
||||
Deployment Instructions
|
||||
1. Never use npm; all commands must use pnpm.
|
||||
2. Run pnpm build.
|
||||
3. Run npx @opennextjs/cloudflare build; if this fails, fix before proceeding.
|
||||
4. Fix any lint errors found (pnpm lint) before deploying.
|
||||
5. Run npx wrangler deploy with no --env flags (uses wrangler.jsonc default route agents.nicholai.work).
|
||||
6. After deploy succeeds, open the site and manually verify UI, tool calls, and custom agent flows.
|
||||
7. Once verification passes, update CLAUDE.md, .cursorrules, and README.md with any new instructions or changes discovered.
|
||||
8. A deploy is only compliant if every step above completes successfully in order.
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,42 @@
|
||||
<!-- Generated by SpecStory, Markdown v2.0.0 -->
|
||||
|
||||
<!-- cursor Session 97b9e83f-b446-4aed-807c-d34788ce8249 (2025-11-15 15:42Z) -->
|
||||
|
||||
# Good git commit workflow for app (2025-11-15 15:42Z)
|
||||
|
||||
_**User (2025-11-15 07:35Z)**_
|
||||
|
||||
for this app in particular, whats a good workflow for commiting changes to git
|
||||
|
||||
---
|
||||
|
||||
_**Agent (model default, mode Ask)**_
|
||||
|
||||
<think><details><summary>Thought Process</summary>
|
||||
**Checking commit workflow instructions**</details></think>
|
||||
|
||||
---
|
||||
|
||||
<tool-use data-tool-type="read" data-tool-name="read_file">
|
||||
<details><summary>Tool use: **read_file** • Read file: /home/nicholai/Documents/dev/multi-agent_chat_interface/.cursor/commands/commit-changes.md</summary>
|
||||
|
||||
</details>
|
||||
</tool-use>
|
||||
|
||||
---
|
||||
|
||||
<think><details><summary>Thought Process</summary>
|
||||
**Preparing final guidance response**</details></think>
|
||||
|
||||
---
|
||||
|
||||
Suggested git workflow for this repo:
|
||||
|
||||
- **Stage prep**: Review `git status` to understand dirty files, then inspect diffs per file (`git diff path/to/file`) to confirm only the intended changes go in.
|
||||
- **Quality gates**: Run `pnpm lint`, relevant tests (`pnpm test`, or targeted suites), and—if UI work—manually verify in browser per UI Development Protocol.
|
||||
- **Summaries**: Update docs or comments if behavior changed; keep `CHANGELOG` or task ticket notes in sync if you use them.
|
||||
- **Commit message**: Compose conventional, task-oriented messages (e.g., `feat: add pinned agent reordering animation`) before committing with `git commit -am "...` or `git commit` after staging via `git add`.
|
||||
- **Final check**: Re-run `git status` to confirm a clean tree, then push or open a PR.
|
||||
|
||||
---
|
||||
|
||||
@ -73,7 +73,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 })
|
||||
}
|
||||
|
||||
const { message, timestamp, sessionId, agentId, images } = body as ChatRequest
|
||||
const { message, timestamp, sessionId, agentId, images, systemPrompt } = body as ChatRequest & { systemPrompt?: string }
|
||||
|
||||
// Get feature flags
|
||||
const flags = getFlags()
|
||||
@ -87,6 +87,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
|
||||
return NextResponse.json({ error: "Agent ID is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate systemPrompt for custom agents
|
||||
if (agentId.startsWith("custom-") && !systemPrompt) {
|
||||
return NextResponse.json(
|
||||
{ error: "systemPrompt is required for custom agents" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if image uploads are enabled
|
||||
if (images && images.length > 0 && !flags.IMAGE_UPLOADS_ENABLED) {
|
||||
return NextResponse.json(
|
||||
@ -109,18 +117,25 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
|
||||
|
||||
console.log("[chat] Sending to webhook:", { agentId, message, timestamp, sessionId })
|
||||
|
||||
const webhookPayload: any = {
|
||||
message,
|
||||
timestamp,
|
||||
sessionId,
|
||||
agentId,
|
||||
images: images && images.length > 0 ? images : undefined,
|
||||
}
|
||||
|
||||
// Include systemPrompt for custom agents
|
||||
if (systemPrompt) {
|
||||
webhookPayload.systemPrompt = systemPrompt
|
||||
}
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
timestamp,
|
||||
sessionId,
|
||||
agentId,
|
||||
images: images && images.length > 0 ? images : undefined,
|
||||
}),
|
||||
body: JSON.stringify(webhookPayload),
|
||||
})
|
||||
|
||||
console.log("[v0] Webhook response status:", response.status)
|
||||
|
||||
@ -40,7 +40,32 @@ export default function Home() {
|
||||
throw new Error(data.error || "Failed to load agents")
|
||||
}
|
||||
|
||||
setAgents(data.agents as typeof agents)
|
||||
const predefinedAgents = data.agents as typeof agents
|
||||
|
||||
// Also load custom agents from localStorage
|
||||
const customAgents: Agent[] = []
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = localStorage.getItem("pinned-agents")
|
||||
if (stored) {
|
||||
try {
|
||||
const pinnedAgents = JSON.parse(stored)
|
||||
customAgents.push(
|
||||
...pinnedAgents.map((pa: any) => ({
|
||||
id: pa.agentId,
|
||||
name: pa.displayName,
|
||||
description: pa.summary,
|
||||
isCustom: true,
|
||||
webhookUrl: "",
|
||||
}))
|
||||
)
|
||||
} catch (err) {
|
||||
console.error("[home] Failed to parse pinned agents:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge predefined and custom agents
|
||||
setAgents([...predefinedAgents, ...customAgents])
|
||||
} catch (err) {
|
||||
setAgents([])
|
||||
setAgentsError(err instanceof Error ? err.message : "Failed to load agents")
|
||||
@ -52,6 +77,33 @@ export default function Home() {
|
||||
fetchAgents()
|
||||
}, [])
|
||||
|
||||
// Listen for pinned agents change and update agents list
|
||||
useEffect(() => {
|
||||
const handlePinnedAgentsChanged = (e: Event) => {
|
||||
const customEvent = e as CustomEvent
|
||||
const updatedPinnedAgents = customEvent.detail?.agents || []
|
||||
|
||||
// Update agents list with new custom agents
|
||||
setAgents((currentAgents) => {
|
||||
// Remove old custom agents and add new ones
|
||||
const predefinedAgents = currentAgents.filter((a) => !a.isCustom)
|
||||
const customAgents = updatedPinnedAgents.map((pa: any) => ({
|
||||
id: pa.agentId,
|
||||
name: pa.displayName,
|
||||
description: pa.summary,
|
||||
isCustom: true,
|
||||
webhookUrl: "",
|
||||
}))
|
||||
return [...predefinedAgents, ...customAgents]
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("pinnedAgentsChanged", handlePinnedAgentsChanged)
|
||||
return () => window.removeEventListener("pinnedAgentsChanged", handlePinnedAgentsChanged)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAgent || agents.length === 0) return
|
||||
const match = agents.find((agent) => agent.id === selectedAgent.id)
|
||||
@ -73,9 +125,47 @@ export default function Home() {
|
||||
}, [agents, selectedAgent])
|
||||
|
||||
const handleAgentSelected = (agent: Agent) => {
|
||||
// Immediately set the selected agent to ensure UI updates
|
||||
setSelectedAgent(agent)
|
||||
localStorage.setItem("selected-agent-id", agent.id)
|
||||
localStorage.setItem("selected-agent", JSON.stringify(agent))
|
||||
|
||||
// Reload agents in the background to pick up any newly pinned custom agents
|
||||
const reloadAgents = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/agents")
|
||||
const data = (await response.json()) as { agents?: unknown; error?: string }
|
||||
if (response.ok && data.agents) {
|
||||
const predefinedAgents = data.agents as typeof agents
|
||||
|
||||
// Load custom agents from localStorage
|
||||
const customAgents: Agent[] = []
|
||||
const stored = localStorage.getItem("pinned-agents")
|
||||
if (stored) {
|
||||
try {
|
||||
const pinnedAgents = JSON.parse(stored)
|
||||
customAgents.push(
|
||||
...pinnedAgents.map((pa: any) => ({
|
||||
id: pa.agentId,
|
||||
name: pa.displayName,
|
||||
description: pa.summary,
|
||||
isCustom: true,
|
||||
webhookUrl: "",
|
||||
}))
|
||||
)
|
||||
} catch (err) {
|
||||
console.error("[home] Failed to parse pinned agents:", err)
|
||||
}
|
||||
}
|
||||
|
||||
setAgents([...predefinedAgents, ...customAgents])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[home] Failed to reload agents:", err)
|
||||
}
|
||||
}
|
||||
|
||||
reloadAgents()
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@ -196,18 +196,29 @@ export function ChatInterface({
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Build request payload
|
||||
const payload: any = {
|
||||
message: userMessage.content,
|
||||
timestamp: userMessage.timestamp.toISOString(),
|
||||
sessionId: sessionId,
|
||||
agentId: agent.id,
|
||||
images: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
}
|
||||
|
||||
// Include systemPrompt for custom agents
|
||||
if (agent.isCustom) {
|
||||
const pinnedAgent = pinnedAgents.find((pa) => pa.agentId === agent.id)
|
||||
if (pinnedAgent) {
|
||||
payload.systemPrompt = pinnedAgent.systemPrompt
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: userMessage.content,
|
||||
timestamp: userMessage.timestamp.toISOString(),
|
||||
sessionId: sessionId,
|
||||
agentId: agent.id,
|
||||
images: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
const data = (await response.json()) as {
|
||||
@ -301,7 +312,28 @@ export function ChatInterface({
|
||||
const handleUseAgentNow = async (agentId: string) => {
|
||||
if (!agentPackage) return
|
||||
|
||||
// Register the agent with the backend
|
||||
// 1. Create PinnedAgent and save to localStorage
|
||||
const pinnedAgent: PinnedAgent = {
|
||||
agentId: agentPackage.agentId,
|
||||
displayName: agentPackage.displayName,
|
||||
summary: agentPackage.summary,
|
||||
tags: agentPackage.tags,
|
||||
recommendedIcon: agentPackage.hints?.recommendedIcon,
|
||||
whenToUse: agentPackage.hints?.whenToUse,
|
||||
systemPrompt: agentPackage.systemPrompt,
|
||||
pinnedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem("pinned-agents")
|
||||
const existing = stored ? JSON.parse(stored) : []
|
||||
const updated = [...existing, pinnedAgent]
|
||||
localStorage.setItem("pinned-agents", JSON.stringify(updated))
|
||||
setPinnedAgents(updated)
|
||||
|
||||
// Dispatch custom event to notify parent component of pinned agent update
|
||||
window.dispatchEvent(new CustomEvent("pinnedAgentsChanged", { detail: { agents: updated } }))
|
||||
|
||||
// 2. Register the agent with the backend
|
||||
try {
|
||||
const response = await fetch("/api/agents/create", {
|
||||
method: "POST",
|
||||
@ -324,25 +356,20 @@ export function ChatInterface({
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary agent object and switch to it
|
||||
// 3. Create a temporary agent object and switch to it
|
||||
const customAgent: Agent = {
|
||||
id: agentId,
|
||||
name: agentPackage.displayName,
|
||||
description: agentPackage.summary,
|
||||
isCustom: true,
|
||||
webhookUrl: "", // Will be handled by custom webhook
|
||||
}
|
||||
|
||||
onAgentSelected(customAgent)
|
||||
// 4. Clear the forge card and close with animation
|
||||
setAgentPackage(null)
|
||||
|
||||
// Add a timeline marker
|
||||
const marker: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: "assistant",
|
||||
content: `✓ Now chatting with **${agentPackage.displayName}**`,
|
||||
timestamp: new Date(),
|
||||
}
|
||||
setMessages((prev) => [...prev, marker])
|
||||
// 5. Switch agent (this will also clear messages due to useEffect watching agent.id)
|
||||
onAgentSelected(customAgent)
|
||||
} catch (error) {
|
||||
console.error("Error registering agent:", error)
|
||||
}
|
||||
@ -392,37 +419,40 @@ export function ChatInterface({
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectPinnedAgent = async (pinnedAgent: PinnedAgent) => {
|
||||
// Register with backend if not already registered
|
||||
try {
|
||||
await fetch("/api/agents/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
agentId: pinnedAgent.agentId,
|
||||
systemPrompt: pinnedAgent.systemPrompt,
|
||||
metadata: {
|
||||
displayName: pinnedAgent.displayName,
|
||||
summary: pinnedAgent.summary,
|
||||
tags: pinnedAgent.tags,
|
||||
recommendedIcon: pinnedAgent.recommendedIcon,
|
||||
whenToUse: pinnedAgent.whenToUse,
|
||||
},
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error registering pinned agent:", error)
|
||||
}
|
||||
|
||||
// Switch to this agent
|
||||
const handleSelectPinnedAgent = (pinnedAgent: PinnedAgent) => {
|
||||
// Switch to this agent FIRST
|
||||
const customAgent: Agent = {
|
||||
id: pinnedAgent.agentId,
|
||||
name: pinnedAgent.displayName,
|
||||
description: pinnedAgent.summary,
|
||||
isCustom: true,
|
||||
webhookUrl: "",
|
||||
}
|
||||
|
||||
onAgentSelected(customAgent)
|
||||
|
||||
// Clear chat history and close drawer
|
||||
setMessages([])
|
||||
setShowPinnedDrawer(false)
|
||||
|
||||
// Register with backend if not already registered (non-blocking, fire and forget)
|
||||
fetch("/api/agents/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
agentId: pinnedAgent.agentId,
|
||||
systemPrompt: pinnedAgent.systemPrompt,
|
||||
metadata: {
|
||||
displayName: pinnedAgent.displayName,
|
||||
summary: pinnedAgent.summary,
|
||||
tags: pinnedAgent.tags,
|
||||
recommendedIcon: pinnedAgent.recommendedIcon,
|
||||
whenToUse: pinnedAgent.whenToUse,
|
||||
},
|
||||
}),
|
||||
}).catch((error) => {
|
||||
console.error("Error registering pinned agent:", error)
|
||||
})
|
||||
}
|
||||
|
||||
const handleComposerAgentSelect = (entry: Agent) => {
|
||||
@ -446,6 +476,7 @@ export function ChatInterface({
|
||||
isOpen={showPinnedDrawer}
|
||||
onClose={() => setShowPinnedDrawer(false)}
|
||||
onSelectAgent={handleSelectPinnedAgent}
|
||||
activeAgentId={agent.id}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
@ -605,47 +636,50 @@ export function ChatInterface({
|
||||
</p>
|
||||
{agents.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 mx-auto max-w-2xl">
|
||||
{agents.map((entry, index) => {
|
||||
const isActive = dropdownSelectedId === entry.id
|
||||
return (
|
||||
<motion.button
|
||||
key={entry.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2 + index * 0.06, duration: 0.4, ease: "easeOut" }}
|
||||
onClick={() => handleComposerAgentSelect(entry)}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]",
|
||||
isActive
|
||||
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
|
||||
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{!isActive && (
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-r from-white/10 via-white/5 to-transparent" />
|
||||
)}
|
||||
<span className="relative">{entry.name}</span>
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
{pinnedAgents.slice(0, 2).map((pinnedAgent, index) => {
|
||||
{agents
|
||||
.filter((entry) => !entry.isCustom) // Filter out custom agents (they show as pills)
|
||||
.map((entry, index) => {
|
||||
const isActive = dropdownSelectedId === entry.id
|
||||
return (
|
||||
<motion.button
|
||||
key={entry.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2 + index * 0.06, duration: 0.4, ease: "easeOut" }}
|
||||
onClick={() => handleComposerAgentSelect(entry)}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]",
|
||||
isActive
|
||||
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
|
||||
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{!isActive && (
|
||||
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-r from-white/10 via-white/5 to-transparent" />
|
||||
)}
|
||||
<span className="relative">{entry.name}</span>
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
{pinnedAgents.map((pinnedAgent, index) => {
|
||||
const isActive = dropdownSelectedId === pinnedAgent.agentId
|
||||
return (
|
||||
<motion.button
|
||||
key={pinnedAgent.agentId}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2 + (agents.length + index) * 0.06, duration: 0.4, ease: "easeOut" }}
|
||||
transition={{ delay: 0.2 + (agents.filter(a => !a.isCustom).length + index) * 0.06, duration: 0.4, ease: "easeOut" }}
|
||||
onClick={() => handleComposerAgentSelect({
|
||||
id: pinnedAgent.agentId,
|
||||
name: pinnedAgent.displayName,
|
||||
description: pinnedAgent.summary || "",
|
||||
isCustom: true,
|
||||
webhookUrl: "" // Custom agents use dynamic routing
|
||||
} as Agent)}
|
||||
className={cn(
|
||||
"rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]",
|
||||
isActive
|
||||
? "bg-white/15 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
|
||||
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
|
||||
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -9,7 +9,8 @@ export interface Agent {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
webhookUrl: string
|
||||
webhookUrl?: string // Optional: not needed for custom agents (routed dynamically)
|
||||
isCustom?: boolean // Flag to distinguish custom agents
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
"AGENT_2_URL": "https://n8n.biohazardvfx.com/webhook/0884bd10-256d-441c-971c-b9f1c8506fdf",
|
||||
"AGENT_2_NAME": "Morgan",
|
||||
"AGENT_2_DESCRIPTION": "System Prompt Designer",
|
||||
"CUSTOM_AGENT_WEBHOOK": "https://n8n.biohazardvfx.com/webhook-test/7cbdc539-526f-425f-abea-0886ec4c1e76",
|
||||
"CUSTOM_AGENT_WEBHOOK": "https://n8n.biohazardvfx.com/webhook/7cbdc539-526f-425f-abea-0886ec4c1e76",
|
||||
"IMAGE_UPLOADS_ENABLED": "true",
|
||||
"DIFF_TOOL_ENABLED": "true"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user