Commit 6270d00b authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

chore: update all changes

parent ff2e9767
......@@ -2,3 +2,9 @@ GA_MEASUREMENT_ID=G-NZW1X7RXTD
GA_API_SECRET=cw05MUKjSWWlZ_NMf8n0Fw
_AUTOCOMPLETE_API=https://anything-copilot.alib.workers.dev/v1/tab/completion
AUTOCOMPLETE_API=http://localhost:8000/v1/tab/completion
# n8n Configuration
N8N_API_URL=https://vuhoanganh1704.app.n8n.cloud
N8N_API_KEY=bgb
MCP_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MDg3MjUyMS1lMWU0LTQyOGItOGJkYi02Y2Y4MTZhM2QxYTkiLCJpc3MiOiJuOG4iLCJhdWQiOiJtY3Atc2VydmVyLWFwaSIsImp0aSI6ImUxNTRmOTU5LTdiZWItNDc0Ny1iZjQ2LTcxZDI5OGMxODNiMyIsImlhdCI6MTc3NzQ0MjQ2M30.Ll2wmRMUB0p3JtIMMSIVtrGVRvTWoNqVfWMSapC9xkA
MCP_PORT=3006
2026/04/29-11:42:37.489 610c Creating DB D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\Local Storage\leveldb since it was missing.
2026/04/29-11:42:37.512 610c Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\Local Storage\leveldb/MANIFEST-000001
2026/04/29-11:42:38.601 610c Creating DB D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\Session Storage since it was missing.
2026/04/29-11:42:38.612 610c Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\Session Storage/MANIFEST-000001
2026/04/29-11:42:37.590 4894 Creating DB D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\shared_proto_db since it was missing.
2026/04/29-11:42:37.602 4894 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\shared_proto_db/MANIFEST-000001
2026/04/29-11:42:37.521 4894 Creating DB D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\shared_proto_db\metadata since it was missing.
2026/04/29-11:42:37.535 4894 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data-2\Default\shared_proto_db\metadata/MANIFEST-000001
2026/04/29-11:37:15.538 6a70 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Local Storage\leveldb/MANIFEST-000001
2026/04/29-11:37:15.544 6a70 Recovering log #3
2026/04/29-11:37:15.545 6a70 Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Local Storage\leveldb/000003.log
2026/04/29-11:13:37.267 66a8 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Local Storage\leveldb/MANIFEST-000001
2026/04/29-11:13:37.273 66a8 Recovering log #3
2026/04/29-11:13:37.275 66a8 Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Local Storage\leveldb/000003.log
2026/04/29-11:38:16.248 6a70 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Session Storage/MANIFEST-000001
2026/04/29-11:38:16.248 6a70 Recovering log #3
2026/04/29-11:38:16.249 6a70 Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Session Storage/000003.log
2026/04/29-11:14:37.269 48e0 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Session Storage/MANIFEST-000001
2026/04/29-11:14:37.270 48e0 Recovering log #3
2026/04/29-11:14:37.271 48e0 Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\Session Storage/000003.log
2026/04/29-11:37:15.649 3710 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db/MANIFEST-000001
2026/04/29-11:37:15.649 3710 Recovering log #3
2026/04/29-11:37:15.650 3710 Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db/000003.log
2026/04/29-11:13:37.302 685c Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db/MANIFEST-000001
2026/04/29-11:13:37.303 685c Recovering log #3
2026/04/29-11:13:37.303 685c Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db/000003.log
2026/04/29-11:37:15.642 3710 Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db\metadata/MANIFEST-000001
2026/04/29-11:37:15.643 3710 Recovering log #3
2026/04/29-11:37:15.643 3710 Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db\metadata/000003.log
2026/04/29-11:13:37.300 685c Reusing MANIFEST D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db\metadata/MANIFEST-000001
2026/04/29-11:13:37.300 685c Recovering log #3
2026/04/29-11:13:37.300 685c Reusing old log D:\cnf\n8n_ai_agent\.tmp\test-user-data\Default\shared_proto_db\metadata/000003.log
......@@ -31,6 +31,7 @@
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.23",
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@playwright/test": "^1.59.1",
"@tsconfig/node18": "^18.2.2",
"@types/lodash-es": "^4.17.11",
"@types/node": "^18.18.5",
......@@ -1834,6 +1835,22 @@
"node": ">=14"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
......@@ -6357,6 +6374,53 @@
"pathe": "^1.1.2"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
......
......@@ -31,29 +31,31 @@ ALWAYS explicitly configure ALL parameters that control node behavior.
## Available MCP Tools
Note: Depending on the connection mode (Local Proxy or Native), tools may or may not have the \`n8n_\` prefix. Use the exact tool names provided by the MCP server.
### Workflow Management
| Tool | Action | Method |
|------|--------|--------|
| n8n_list_workflows | List all workflows | GET /workflows |
| n8n_get_workflow | Get workflow JSON (full/structure/minimal) | GET /workflows/{id} |
| n8n_create_workflow | Create new workflow | POST /workflows |
| n8n_update_workflow | Full workflow update (replace) | PUT /workflows/{id} |
| n8n_delete_workflow | Delete workflow permanently | DELETE /workflows/{id} |
| n8n_activate_workflow | Activate (start triggers) | POST /workflows/{id}/activate |
| n8n_deactivate_workflow | Deactivate (stop triggers) | POST /workflows/{id}/deactivate |
| Tool | Action |
|------|--------|
| [n8n_]list_workflows | List all workflows |
| [n8n_]get_workflow | Get workflow JSON (full/structure/minimal) |
| [n8n_]create_workflow | Create new workflow |
| [n8n_]update_workflow | Full workflow update (replace) |
| [n8n_]delete_workflow | Delete workflow permanently |
| [n8n_]activate_workflow | Activate (start triggers) |
| [n8n_]deactivate_workflow | Deactivate (stop triggers) |
### Execution Management
| Tool | Action |
|------|--------|
| n8n_execute_workflow | Trigger workflow with input data |
| n8n_list_executions | List recent executions (filter by workflow/status) |
| n8n_get_execution | Get execution details with node I/O data |
| [n8n_]execute_workflow | Trigger workflow with input data |
| [n8n_]list_executions | List recent executions (filter by workflow/status) |
| [n8n_]get_execution | Get execution details with node I/O data |
### System
| Tool | Action |
|------|--------|
| n8n_list_credentials | List credential names/types (values never exposed) |
| n8n_health_check | Check n8n API connectivity |
| [n8n_]list_credentials | List credential names/types (values never exposed) |
| [n8n_]health_check | Check n8n API connectivity |
## Workflow Modification Process
......
......@@ -86,7 +86,7 @@ async function openPipBackground(url: string) {
url: url,
mode: "write-html",
},
})
}).catch(err => console.debug("[bg] openPipBackground sendMessage failed:", err))
}
async function pipLaunch(url: string) {
......@@ -98,7 +98,7 @@ async function pipLaunch(url: string) {
chrome.tabs.sendMessage(tab.id!, {
type: MessageType.pipLaunch,
url: url,
})
}).catch(err => console.debug("[bg] pipLaunch sendMessage failed:", err))
}
type UpdatePipWinOption = {
......@@ -136,20 +136,23 @@ async function handleInvokeRequest(
currentSender = sender
let res = await messageInvoke.handleReqMsg(message)
if (!sender.tab?.id) {
console.error("sender tab id is undefined", sender)
}
if (res) {
chrome.tabs.sendMessage(sender.tab?.id!, {
if (sender.tab?.id) {
chrome.tabs.sendMessage(sender.tab.id, {
type: MessageType.invokeResponse,
...res,
}).catch(err => console.debug("[bg] handleInvokeRequest sendMessage failed:", err))
} else {
chrome.runtime.sendMessage({
type: MessageType.invokeResponse,
...res,
})
}
}
}
function handleMessage(message: any, sender: chrome.runtime.MessageSender, sendResponse?: (response: any) => void) {
console.log("[bg]: ", message.type, message, sender, Date.now())
console.log("[bg]: ", message?.type, message, sender, Date.now())
// ── SuperAssistant MCP Bridge ──
if (message?.type === "n8n-mcp:call-tool") {
......@@ -169,6 +172,13 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender, sendR
return true // Async response
}
// ── MCP Connection Status ──
if (message?.type === "n8n-mcp:status") {
const status = mcpService.getStatus()
sendResponse?.({ connected: status === "connected", status })
return false
}
// ── MCP Config Management ──
if (message?.type === "n8n-mcp:get-config") {
chrome.storage.local.get(["mcp_n8n_config"]).then((data) => {
......@@ -176,7 +186,8 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender, sendR
sendResponse?.({
proxyUrl: cfg.proxyUrl || "http://localhost:3006",
n8nApiUrl: cfg.n8nApiUrl || "",
n8nApiKey: cfg.n8nApiKey || ""
n8nApiKey: cfg.n8nApiKey || "",
mcpAuthToken: cfg.mcpAuthToken || ""
})
})
return true
......@@ -250,11 +261,12 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender, sendR
break
case MessageType.forwardToTab:
chrome.tabs.sendMessage(message.tabId, message.message)
.catch(err => console.debug("[bg] forwardToTab sendMessage failed:", err))
break
case MessageType.invokeRequest:
currentSender = sender
messageInvoke.handleReqMsg(message, sender)
break
return true
case MessageType.invokeResponse:
messageInvoke.handleResMsg(message)
break
......@@ -265,6 +277,12 @@ function handleMessage(message: any, sender: chrome.runtime.MessageSender, sendR
registerContentSidebar(sender.tab!.id!, message.info)
break
}
// Call sendResponse for non-async paths to avoid port closure errors
if (sendResponse) {
sendResponse({ handled: true })
}
return false
}
async function toggleMinimize() {
......
<script setup lang="ts">
import { ref, onMounted, computed } from "vue"
import { ref, onMounted, computed, onUnmounted } from "vue"
import { openSidebar, getIsEdge } from "@/utils/ext"
// ─── State ──────────────────────────────────────────────────────────
......@@ -15,6 +16,7 @@ const workflowLoaded = ref(false)
// MCP connection
const mcpConnected = ref(false)
let statusInterval: any = null
// Platform selection
const selectedPlatform = ref<string | null>(null)
......@@ -51,9 +53,9 @@ const platforms = [
onMounted(async () => {
try {
// 1. Check MCP connection
const mcpStatus = await sendMessage({ type: "n8n-mcp:status" })
mcpConnected.value = mcpStatus?.connected || false
// 1. Start status polling
checkMcpStatus()
statusInterval = setInterval(checkMcpStatus, 2000)
// 2. Check active tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
......@@ -67,6 +69,23 @@ onMounted(async () => {
}
})
onUnmounted(() => {
if (statusInterval) clearInterval(statusInterval)
})
async function checkMcpStatus() {
const mcpStatus = await sendMessage({ type: "n8n-mcp:status" })
const newStatus = mcpStatus?.connected || false
// If connection status just became true and we don't have workflow info, re-detect
if (newStatus && !mcpConnected.value && isN8nPage.value && !workflowLoaded.value) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
if (tab) detectN8nWorkflow(tab)
}
mcpConnected.value = newStatus
}
// ─── n8n Detection ──────────────────────────────────────────────────
async function detectN8nWorkflow(tab: chrome.tabs.Tab) {
......@@ -158,11 +177,13 @@ async function launchPlatform(platform: typeof platforms[0]) {
selectedPlatform.value = platform.id
try {
// Open new tab with the AI platform
const newTab = await chrome.tabs.create({ url: platform.url })
const isEdge = getIsEdge()
// The content script (superassistant/index.ts) on the new tab will
// automatically fetch context from background and inject it into chat
// Open AI platform in sidebar
await openSidebar({
urls: [platform.url],
sidePanel: !isEdge, // Use Chrome SidePanel if available, otherwise content sidebar
})
// Close popup
setTimeout(() => window.close(), 300)
......
......@@ -45,24 +45,39 @@ const showSettings = ref(false)
const mcpConfig = reactive({
proxyUrl: "http://localhost:3006",
n8nApiUrl: "",
n8nApiKey: ""
mcpAuthToken: ""
})
const isSavingConfig = ref(false)
const saveMessage = ref("")
async function saveMcpConfig() {
isSavingConfig.value = true
saveMessage.value = "⏳ Đang kết nối thử..."
await chrome.runtime.sendMessage({
type: "n8n-mcp:set-config",
config: {
proxyUrl: mcpConfig.proxyUrl,
n8nApiUrl: mcpConfig.n8nApiUrl,
n8nApiKey: mcpConfig.n8nApiKey
mcpAuthToken: mcpConfig.mcpAuthToken
}
})
// Wait a bit for connection attempt
setTimeout(async () => {
const status = await chrome.runtime.sendMessage({ type: "n8n-mcp:status" })
if (status?.connected) {
saveMessage.value = "✅ Kết nối thành công!"
setTimeout(() => {
isSavingConfig.value = false
showSettings.value = false
}, 500)
saveMessage.value = ""
}, 1500)
} else {
saveMessage.value = "❌ Thất bại. Kiểm tra lại Token!"
isSavingConfig.value = false
}
}, 3000)
}
const host = computed({
......@@ -128,6 +143,7 @@ onMounted(() => {
mcpConfig.proxyUrl = res.proxyUrl || "http://localhost:3006"
mcpConfig.n8nApiUrl = res.n8nApiUrl || ""
mcpConfig.n8nApiKey = res.n8nApiKey || ""
mcpConfig.mcpAuthToken = res.mcpAuthToken || ""
}
})
......@@ -296,12 +312,12 @@ function showChatDocs() {
/>
</div>
<div>
<label class="text-xs opacity-60 mb-1 block">n8n API Key</label>
<label class="text-xs opacity-60 mb-1 block">MCP Auth Token (Bearer)</label>
<input
v-model="mcpConfig.n8nApiKey"
v-model="mcpConfig.mcpAuthToken"
type="password"
class="w-full bg-background border border-white/10 rounded px-2 py-1 text-sm outline-none focus:border-blue-500 transition-colors"
placeholder="n8n_api_key_..."
placeholder="eyJhbGci..."
/>
</div>
<button
......@@ -309,8 +325,11 @@ function showChatDocs() {
:disabled="isSavingConfig"
class="mt-1 w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded py-1.5 text-sm font-medium transition-colors"
>
{{ isSavingConfig ? "Saving..." : "Save Settings" }}
{{ isSavingConfig ? "Checking..." : "Save Settings" }}
</button>
<div v-if="saveMessage" class="mt-2 text-center text-xs font-medium" :class="saveMessage.includes('✅') ? 'text-green-500' : 'text-amber-500'">
{{ saveMessage }}
</div>
</div>
</div>
......
This diff is collapsed.
......@@ -63,6 +63,34 @@ async function initialize(): Promise<void> {
logMessage(`SuperAssistant ready on ${adapter.name} ⚡`)
}
// ─── System Prompt Fallback ─────────────────────────────────────────
const SYSTEM_PROMPT_FALLBACK = `You are an expert n8n workflow automation engineer with direct API access to a live n8n instance through MCP (Model Context Protocol) tools. Your role is to design, build, modify, validate, and execute n8n workflows with maximum accuracy and efficiency.
## Available MCP Tools
Note: Since you are connected directly to n8n, tools DO NOT have the 'n8n_' prefix.
- list_workflows: List all workflows
- get_workflow: Get workflow JSON (full/structure/minimal)
- create_workflow: Create new workflow
- update_workflow: Full workflow update (replace)
- execute_workflow: Trigger workflow with input data
- list_credentials: List credential names/types
## Workflow Modification Process
1. Read Current State (get_workflow)
2. Understand Structure (identify nodes and connections)
3. Plan Changes (tell user)
4. Build & Update (update_workflow with COMPLETE JSON)
5. Verify (get_workflow structure)
## Critical Warnings
- Use \`={{ $json.field }}\` syntax for expressions.
- ALWAYS use the LATEST stable typeVersion for each node.
- Connections use SOURCE NODE NAME as key.
Respond in the SAME LANGUAGE as the user's message.
`;
// ─── Context Injection ──────────────────────────────────────────────
/**
......@@ -84,18 +112,24 @@ async function injectWorkflowContext(adapter: any): Promise<void> {
// Store context globally for bridge to auto-attach to tool calls
;(window as any).__n8nWorkflowContext = ctx
// Fetch system prompt from MCP proxy
let systemPrompt = ""
// Fetch system prompt from MCP proxy (optional fallback)
let systemPrompt = SYSTEM_PROMPT_FALLBACK
try {
const mcpConfig = await sendMessage({ type: "n8n-mcp:get-config" })
const proxyUrl = mcpConfig?.proxyUrl || "http://localhost:3006"
// If using local proxy, try to fetch its custom prompt
if (proxyUrl.includes("localhost") || proxyUrl.includes("127.0.0.1")) {
const res = await fetch(`${proxyUrl}/system-prompt`)
if (res.ok) {
systemPrompt = await res.text()
logger.info("✅ System prompt loaded from MCP proxy")
logger.info("✅ System prompt loaded from local proxy")
}
} else {
logger.info("ℹ️ Native MCP detected — using embedded system prompt")
}
} catch (err) {
logger.info("⚠️ Could not fetch system prompt — using inline fallback")
logger.info("⚠️ Using embedded system prompt fallback")
}
// Build the final prompt: system prompt + workflow context
......@@ -108,6 +142,7 @@ async function injectWorkflowContext(adapter: any): Promise<void> {
const inserted = await adapter.insertText(prompt)
if (inserted) {
logger.info("✅ Workflow context injected into chat input")
// Trigger auto-send if the prompt is large (optional, but better for UX)
} else {
logger.info("⚠️ Could not inject context — chat input not found (user may type manually)")
}
......@@ -115,33 +150,29 @@ async function injectWorkflowContext(adapter: any): Promise<void> {
function buildContextPrompt(ctx: any, systemPrompt: string): string {
const contextBlock = [
`I'm working on an n8n workflow.`,
`--- WORKFLOW CONTEXT ---`,
`I'm working on this n8n workflow:`,
`ID: ${ctx.workflowId}`,
`Name: ${ctx.workflowName}`,
`URL: ${ctx.n8nBaseUrl}/workflow/${ctx.workflowId}`,
``,
`**Workflow ID:** ${ctx.workflowId}`,
`**Name:** ${ctx.workflowName}`,
`**Instance:** ${ctx.n8nBaseUrl}`,
``,
`**Current structure:**`,
`Current structure (JSON):`,
"```json",
ctx.workflowSummary,
"```",
`-----------------------`,
``,
`Please help me modify this workflow. Use the n8n MCP tools ` +
`(n8n_get_workflow, n8n_update_workflow, etc.) targeting workflow ID \`${ctx.workflowId}\`.`,
`I want you to help me with this workflow. Please use the MCP tools available to you to read or modify it.`,
].join("\n")
// If system prompt available, prepend it (hidden in a system block)
if (systemPrompt) {
return [
`<system>`,
`# INSTRUCTIONS`,
systemPrompt,
`</system>`,
``,
contextBlock,
``,
`My request: `,
].join("\n")
}
return contextBlock
}
// ─── Helpers ────────────────────────────────────────────────────────
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment