diff --git a/.gitignore b/.gitignore
index 3da0266..bf00d93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,6 @@ client_secret_*.apps.googleusercontent.com.json
node_modules/
*.pyc
-# Desktop: Tauri frontend bundle (tauri.conf.json build.frontendDist)
-apps/desktop/dist/
-
# Tauri app icon pack: generated by `cargo tauri icon apps/desktop/src-tauri/icons/icon.png`
# Version control only the 1024×1024 master; regenerate the rest locally or in release builds.
apps/desktop/src-tauri/icons/**
diff --git a/AGENTS.md b/AGENTS.md
index 3f126fb..2467345 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -199,7 +199,7 @@ http://127.0.0.1:9515/mcp
### 图标与前端 dist(本地 / CI)
-版本库为减小噪音,**不提交** Tauri 生成的多尺寸图标包,以及 **`apps/desktop/dist/`** 前端打包目录(见根目录 `.gitignore`)。
+版本库为减小噪音,**不提交** Tauri 生成的多尺寸图标包;但 **`apps/desktop/dist/`** 现在作为桌面端前端静态资源目录,**需要提交到版本库**,以保证新机器 clone 后可直接运行 Tauri desktop。
- **图标**:仅跟踪 `apps/desktop/src-tauri/icons/icon.png` 作为源图(建议 **1024×1024** PNG)。检出代码后,若需要完整 `icons/`(例如打包、验证窗口/托盘图标),在 **`apps/desktop/src-tauri`** 下执行:
@@ -210,7 +210,7 @@ http://127.0.0.1:9515/mcp
需已安装 **Tauri CLI**(例如 `cargo install tauri-cli`,或与项目一致的 `cargo-tauri` 版本)。
-- **前端 dist**:`tauri.conf.json` 中 `build.frontendDist` 指向 `../dist`。本地或 CI 在运行 `cargo tauri dev` / `cargo tauri build` 前,需先按项目约定生成或同步 **`apps/desktop/dist/`** 内容;流水线构建桌面端时,在 Tauri 步骤之前加入对应的前端产物步骤即可。
+- **前端 dist**:`tauri.conf.json` 中 `build.frontendDist` 指向 `../dist`。当前仓库直接跟踪 **`apps/desktop/dist/`** 下的静态页面资源,因此新机器 clone 后无需额外生成前端产物即可运行 `cargo run -p secrets-desktop`。若后续引入独立前端构建链,再单独把这部分切回构建产物管理。
## 代码规范
diff --git a/README.md b/README.md
index a2c447c..9290a64 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,12 @@ cargo run -p secrets-desktop-daemon
cargo run -p secrets-desktop
```
+说明:
+
+- `apps/desktop/src-tauri/tauri.conf.json` 中 `build.frontendDist` 指向 `apps/desktop/dist`
+- 当前仓库会直接提交 `apps/desktop/dist/` 下的桌面端静态资源
+- 因此新机器 clone 后,无需额外前端构建步骤即可启动 desktop
+
## 当前能力
- 桌面端使用系统浏览器完成 Google Desktop OAuth 登录
diff --git a/apps/desktop/dist/disable-features.js b/apps/desktop/dist/disable-features.js
new file mode 100644
index 0000000..d907376
--- /dev/null
+++ b/apps/desktop/dist/disable-features.js
@@ -0,0 +1,41 @@
+(() => {
+ const tauriInvoke = window.__TAURI_INTERNALS__?.invoke;
+
+ // Disable text selection globally, but keep inputs editable.
+ document.addEventListener("selectstart", (event) => {
+ const target = event.target;
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
+ return;
+ }
+ event.preventDefault();
+ });
+
+ async function applyProductionGuards() {
+ if (!tauriInvoke) {
+ return;
+ }
+
+ let isDebugBuild = false;
+ try {
+ isDebugBuild = await tauriInvoke("is_debug_build");
+ } catch {
+ return;
+ }
+
+ if (isDebugBuild) {
+ return;
+ }
+
+ document.addEventListener("contextmenu", (event) => event.preventDefault());
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "F12") {
+ event.preventDefault();
+ }
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && ["I", "C", "J"].includes(event.key.toUpperCase())) {
+ event.preventDefault();
+ }
+ });
+ }
+
+ void applyProductionGuards();
+})();
diff --git a/apps/desktop/dist/favicon.png b/apps/desktop/dist/favicon.png
new file mode 100644
index 0000000..35f1555
Binary files /dev/null and b/apps/desktop/dist/favicon.png differ
diff --git a/apps/desktop/dist/index.html b/apps/desktop/dist/index.html
new file mode 100644
index 0000000..c492f99
--- /dev/null
+++ b/apps/desktop/dist/index.html
@@ -0,0 +1,279 @@
+
+
+
+
+
+ Secrets
+
+
+
+
+
+
+
+
+
+
+
Secrets
+
用 AI 安全地管理和使用密钥
+
+
+
+
+
+
+
+
+
+
+
+
+
请输入本地 vault 主密码。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
查看已登录设备的在线情况与最近活动。
+
+
+
+
+
+
+
+
查看当前 AI 工具的 MCP 集成情况,并一键写入本地 daemon 配置。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/dist/main.js b/apps/desktop/dist/main.js
new file mode 100644
index 0000000..08cef8b
--- /dev/null
+++ b/apps/desktop/dist/main.js
@@ -0,0 +1,1018 @@
+const invoke = window.__TAURI_INTERNALS__?.invoke;
+
+const loginView = document.getElementById("login-view");
+const appShell = document.getElementById("app-shell");
+const loginButton = document.getElementById("login-button");
+const loginError = document.getElementById("login-error");
+const vaultModal = document.getElementById("vault-modal");
+const vaultModalTitle = document.getElementById("vault-modal-title");
+const vaultModalCopy = document.getElementById("vault-modal-copy");
+const vaultPasswordInput = document.getElementById("vault-password-input");
+const vaultModalError = document.getElementById("vault-modal-error");
+const vaultModalSave = document.getElementById("vault-modal-save");
+const logoutButton = document.getElementById("logout-button");
+const userTrigger = document.getElementById("user-trigger");
+const userMenu = document.getElementById("user-menu");
+const manageDevicesButton = document.getElementById("manage-devices");
+const openMcpModalButton = document.getElementById("open-mcp-modal");
+const deviceModal = document.getElementById("device-modal");
+const deviceList = document.getElementById("device-list");
+const closeDeviceModal = document.getElementById("close-device-modal");
+const mcpModal = document.getElementById("mcp-modal");
+const closeMcpModal = document.getElementById("close-mcp-modal");
+const mcpIntegrationList = document.getElementById("mcp-integration-list");
+const mcpConfig = document.getElementById("mcp-config");
+const copyMcpConfigButton = document.getElementById("copy-mcp-config");
+const copyMcpConfigButtonLabel = copyMcpConfigButton.querySelector(".button-label");
+
+const userName = document.getElementById("user-name");
+const userEmail = document.getElementById("user-email");
+const folderList = document.getElementById("folder-list");
+const entryList = document.getElementById("entry-list");
+const detailFolderLabel = document.getElementById("detail-folder-label");
+const entryTitle = document.getElementById("entry-title");
+const metadataList = document.getElementById("metadata-list");
+const secretList = document.getElementById("secret-list");
+const searchInput = document.getElementById("search-input");
+const typeFilter = document.getElementById("type-filter");
+const detailBadge = document.getElementById("detail-badge");
+const editEntryButton = document.getElementById("edit-entry-button");
+const deleteEntryButton = document.getElementById("delete-entry-button");
+const restoreEntryButton = document.getElementById("restore-entry-button");
+const saveEntryButton = document.getElementById("save-entry-button");
+const cancelEditButton = document.getElementById("cancel-edit-button");
+const addSecretButton = document.getElementById("add-secret-button");
+const newEntryButton = document.getElementById("new-entry-button");
+const nameSection = document.getElementById("name-section");
+const nameView = document.getElementById("name-view");
+const nameInput = document.getElementById("name-input");
+const metadataEditor = document.getElementById("metadata-editor");
+const addMetadataButton = document.getElementById("add-metadata-button");
+
+const entryModal = document.getElementById("entry-modal");
+const closeEntryModal = document.getElementById("close-entry-modal");
+const entryModalCancel = document.getElementById("entry-modal-cancel");
+const entryModalSave = document.getElementById("entry-modal-save");
+const entryModalFolder = document.getElementById("entry-modal-folder");
+const entryModalTitle = document.getElementById("entry-modal-title");
+const entryModalType = document.getElementById("entry-modal-type");
+
+const secretModal = document.getElementById("secret-modal");
+const closeSecretModal = document.getElementById("close-secret-modal");
+const secretModalCancel = document.getElementById("secret-modal-cancel");
+const secretModalSave = document.getElementById("secret-modal-save");
+const secretModalTitle = document.getElementById("secret-modal-title");
+const secretNameInput = document.getElementById("secret-name-input");
+const secretTypeInput = document.getElementById("secret-type-input");
+const secretValueInput = document.getElementById("secret-value-input");
+
+const historyModal = document.getElementById("history-modal");
+const closeHistoryModal = document.getElementById("close-history-modal");
+const historyModalCopy = document.getElementById("history-modal-copy");
+const historyList = document.getElementById("history-list");
+
+const appState = {
+ shell: null,
+ activeFolder: "所有项目",
+ activeType: "",
+ query: "",
+ mcpJson: "",
+ editing: false,
+ draftMetadata: [],
+ revealedSecrets: {},
+ secretModalMode: "create",
+ editingSecretId: null,
+ historySecretId: null,
+};
+
+function escapeHtml(value) {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function displaySecretName(name) {
+ const normalized = String(name ?? "")
+ .trim()
+ .toLowerCase()
+ .replaceAll("_", " ");
+ if (normalized === "api token" || normalized === "token") return "访问令牌";
+ return String(name ?? "");
+}
+
+function integrationGlyph(appName) {
+ if (appName === "Cursor") return "⌘";
+ if (appName === "Claude Code") return ">_";
+ return "⌁";
+}
+
+function selectedEntry() {
+ return appState.shell?.selected_entry || null;
+}
+
+function resetSecretCache() {
+ appState.revealedSecrets = {};
+}
+
+function setSelectedDetail(detail) {
+ if (!appState.shell) return;
+ appState.shell.selected_entry_id = detail?.id || null;
+ appState.shell.selected_entry = detail;
+ appState.editing = false;
+ appState.draftMetadata = detail?.metadata ? detail.metadata.map((item) => ({ ...item })) : [];
+ resetSecretCache();
+}
+
+function showLogin() {
+ loginView.classList.remove("hidden");
+ appShell.classList.add("hidden");
+}
+
+function showShell() {
+ loginView.classList.add("hidden");
+ appShell.classList.remove("hidden");
+}
+
+function setLoginError(message) {
+ loginError.textContent = message || "";
+ loginError.classList.toggle("hidden", !message);
+}
+
+function setVaultModalError(message) {
+ vaultModalError.textContent = message || "";
+ vaultModalError.classList.toggle("hidden", !message);
+}
+
+function openVaultModal({ setup = false } = {}) {
+ vaultModalTitle.textContent = setup ? "设置本地 Vault 主密码" : "解锁本地 Vault";
+ vaultModalCopy.textContent = setup
+ ? "首次使用,请先设置本地 vault 主密码。"
+ : "请输入本地 vault 主密码以解锁。";
+ vaultPasswordInput.value = "";
+ setVaultModalError("");
+ vaultModal.classList.remove("hidden");
+ setTimeout(() => vaultPasswordInput.focus(), 0);
+}
+
+function closeVaultModal() {
+ vaultModal.classList.add("hidden");
+ vaultPasswordInput.value = "";
+ setVaultModalError("");
+}
+
+function setAvatar() {
+ const avatar = document.querySelector(".avatar");
+ const initial = appState.shell?.user?.name?.trim()?.[0] || "S";
+ avatar.textContent = initial.toUpperCase();
+}
+
+function renderFolders() {
+ if (!appState.shell) return;
+ userName.textContent = appState.shell.user.name;
+ userEmail.textContent = appState.shell.user.email;
+ setAvatar();
+ const activeFolder = appState.activeFolder;
+ const folders = [
+ {
+ label: "所有项目",
+ count: appState.shell.folders
+ .filter((folder) => folder.kind === "folder")
+ .reduce((sum, folder) => sum + folder.count, 0),
+ },
+ ...appState.shell.folders,
+ ];
+ folderList.innerHTML = folders
+ .map(
+ (folder) => `
+
+ `
+ )
+ .join("");
+}
+
+function renderTypeFilter() {
+ if (!appState.shell) return;
+ typeFilter.innerHTML = [
+ ``,
+ ...appState.shell.entry_types.map(
+ (entryType) =>
+ ``
+ ),
+ ].join("");
+}
+
+function renderEntries() {
+ if (!appState.shell) return;
+ const entries = appState.shell.entries;
+ const selectedId = appState.shell.selected_entry_id;
+ if (!entries.length) {
+ entryList.innerHTML = `
+
+
没有匹配的条目
+
调整搜索词、类型筛选或文件夹后再试。
+
+ `;
+ return;
+ }
+ entryList.innerHTML = entries
+ .map(
+ (entry) => `
+
+ `
+ )
+ .join("");
+}
+
+function renderMetadataEditor() {
+ metadataEditor.innerHTML = appState.draftMetadata
+ .map(
+ (field, index) => `
+
+
+
+
+
+ `
+ )
+ .join("");
+}
+
+function syncEditMode() {
+ const editing = appState.editing;
+ const hasSelection = Boolean(selectedEntry());
+ const deleted = Boolean(selectedEntry()?.deleted);
+ editEntryButton.classList.toggle("hidden", editing || !hasSelection || deleted);
+ saveEntryButton.classList.toggle("hidden", !editing);
+ cancelEditButton.classList.toggle("hidden", !editing);
+ nameSection.classList.toggle("hidden", !editing);
+ nameView.classList.add("hidden");
+ nameInput.classList.toggle("hidden", !editing);
+ metadataList.classList.toggle("hidden", editing);
+ metadataEditor.classList.toggle("hidden", !editing);
+ addMetadataButton.classList.toggle("hidden", !editing);
+ addSecretButton.classList.toggle("hidden", editing || !hasSelection || deleted);
+}
+
+function renderSecretCard(secret) {
+ const revealed = appState.revealedSecrets[secret.id];
+ const isVisible = Boolean(revealed?.visible);
+ const value = isVisible ? revealed.value : secret.masked_value;
+ return `
+
+
${escapeHtml(displaySecretName(secret.name))}
+
+
+
🔒
+
${escapeHtml(value)}
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+function renderDetail() {
+ const detail = selectedEntry();
+ if (!detail) {
+ detailFolderLabel.textContent = "-";
+ entryTitle.textContent = "未选择条目";
+ nameView.textContent = "-";
+ nameInput.value = "";
+ metadataList.innerHTML = `从左侧列表选择一个条目后,这里会显示结构化元数据。
`;
+ metadataEditor.innerHTML = "";
+ secretList.innerHTML = ``;
+ detailBadge.classList.add("hidden");
+ deleteEntryButton.classList.add("hidden");
+ restoreEntryButton.classList.add("hidden");
+ syncEditMode();
+ return;
+ }
+
+ detailFolderLabel.textContent = detail.folder;
+ entryTitle.textContent = detail.title;
+ nameView.textContent = detail.title;
+ nameInput.value = detail.title;
+ detailBadge.classList.toggle("hidden", !detail.deleted);
+ deleteEntryButton.classList.toggle("hidden", detail.deleted);
+ restoreEntryButton.classList.toggle("hidden", !detail.deleted);
+ metadataList.innerHTML = detail.metadata.length
+ ? detail.metadata
+ .map(
+ (field) => `
+
+
${escapeHtml(field.label)}
+
${escapeHtml(field.value)}
+
+ `
+ )
+ .join("")
+ : ``;
+ renderMetadataEditor();
+
+ secretList.innerHTML = detail.secrets.length
+ ? detail.secrets.map(renderSecretCard).join("")
+ : ``;
+ syncEditMode();
+}
+
+function renderShell(shell) {
+ appState.shell = shell;
+ appState.activeFolder = "所有项目";
+ appState.activeType = "";
+ appState.query = "";
+ appState.editing = false;
+ appState.draftMetadata = shell.selected_entry?.metadata
+ ? shell.selected_entry.metadata.map((item) => ({ ...item }))
+ : [];
+ resetSecretCache();
+ searchInput.value = "";
+ renderFolders();
+ renderTypeFilter();
+ renderEntries();
+ renderDetail();
+}
+
+async function ensureUnlockedShell(data) {
+ if (!data?.logged_in) return data;
+ if (data?.shell) return data;
+
+ const vault = data?.vault;
+ if (!vault) return data;
+
+ return new Promise((resolve, reject) => {
+ const setup = !vault.has_master_password;
+ openVaultModal({ setup });
+
+ const submit = async () => {
+ const password = vaultPasswordInput.value.trim();
+ if (!password) {
+ setVaultModalError(setup ? "请先设置本地 vault 主密码" : "请输入本地 vault 主密码");
+ return;
+ }
+
+ vaultModalSave.disabled = true;
+ try {
+ if (setup) {
+ await invoke("setup_master_password", { password });
+ } else {
+ await invoke("unlock_vault", { password });
+ }
+ cleanup();
+ closeVaultModal();
+ const bootstrapped = await invoke("app_bootstrap");
+ resolve(bootstrapped);
+ } catch (error) {
+ setVaultModalError(String(error));
+ } finally {
+ vaultModalSave.disabled = false;
+ }
+ };
+
+ const onClick = () => {
+ void submit();
+ };
+ const onKeydown = (event) => {
+ if (event.key !== "Enter") return;
+ event.preventDefault();
+ void submit();
+ };
+ const cleanup = () => {
+ vaultModalSave.removeEventListener("click", onClick);
+ vaultPasswordInput.removeEventListener("keydown", onKeydown);
+ };
+
+ vaultModalSave.addEventListener("click", onClick);
+ vaultPasswordInput.addEventListener("keydown", onKeydown);
+ });
+}
+
+function renderMcpDialog(data) {
+ appState.mcpJson = data.mcp_json;
+ mcpIntegrationList.innerHTML = data.integrations
+ .map(
+ (integration) => `
+
+
+
${integrationGlyph(integration.app_name)}
+
+
${escapeHtml(integration.app_name)}
+
+
+
+
+ `
+ )
+ .join("");
+ mcpConfig.textContent = data.mcp_json;
+}
+
+function closeEntryModalPanel() {
+ entryModal.classList.add("hidden");
+ entryModalFolder.value = "";
+ entryModalTitle.value = "";
+ entryModalType.value = "";
+}
+
+function openEntryModalPanel() {
+ const detail = selectedEntry();
+ entryModalFolder.value = detail?.folder || "Refining";
+ entryModalType.value = detail?.entry_type || "service";
+ entryModalTitle.value = "";
+ entryModal.classList.remove("hidden");
+}
+
+function closeSecretModalPanel() {
+ secretModal.classList.add("hidden");
+ appState.secretModalMode = "create";
+ appState.editingSecretId = null;
+ secretNameInput.value = "";
+ secretTypeInput.value = "text";
+ secretValueInput.value = "";
+}
+
+function openSecretModalPanel(mode, options = {}) {
+ appState.secretModalMode = mode;
+ appState.editingSecretId = options.secretId || null;
+ secretModalTitle.textContent = mode === "edit" ? "编辑密钥" : "新增密钥";
+ secretNameInput.value = options.name || "";
+ secretTypeInput.value = options.secretType || "text";
+ secretValueInput.value = options.value || "";
+ secretModal.classList.remove("hidden");
+}
+
+function closeHistoryModalPanel() {
+ historyModal.classList.add("hidden");
+ appState.historySecretId = null;
+ historyList.innerHTML = "";
+}
+
+function setTransientLabel(labelNode, nextText, originalText) {
+ if (!labelNode) return;
+ labelNode.textContent = nextText;
+ setTimeout(() => {
+ labelNode.textContent = originalText;
+ }, 1200);
+}
+
+async function bootstrap() {
+ if (!invoke) {
+ showLogin();
+ return;
+ }
+
+ try {
+ let data = await invoke("app_bootstrap");
+ if (!data.logged_in) {
+ showLogin();
+ return;
+ }
+ data = await ensureUnlockedShell(data);
+
+ renderShell(data.shell);
+ showShell();
+ } catch (error) {
+ setLoginError(String(error));
+ showLogin();
+ }
+}
+
+async function doDemoLogin() {
+ if (!invoke) return;
+ setLoginError("");
+ loginButton.disabled = true;
+ try {
+ let data = await invoke("continue_demo_login");
+ data = await ensureUnlockedShell(data);
+ renderShell(data.shell);
+ showShell();
+ } catch (error) {
+ setLoginError(String(error));
+ } finally {
+ loginButton.disabled = false;
+ }
+}
+
+async function doLogout() {
+ if (!invoke) return;
+ await invoke("logout");
+ userMenu.classList.add("hidden");
+ appState.shell = null;
+ showLogin();
+}
+
+async function loadEntryDetail(entryId) {
+ const detail = await invoke("entry_detail", { entryId });
+ setSelectedDetail(detail);
+ renderEntries();
+ renderDetail();
+}
+
+async function refreshEntries() {
+ if (!appState.shell) return;
+ const deletedOnly = appState.activeFolder === "最近删除";
+ const folder =
+ appState.activeFolder === "所有项目" || deletedOnly
+ ? null
+ : appState.activeFolder;
+ const query = appState.query.trim() || null;
+ const entries = await invoke("list_entries", {
+ query: {
+ folder,
+ entryType: appState.activeType || null,
+ query,
+ deletedOnly,
+ },
+ });
+ appState.shell.entries = entries;
+ renderEntries();
+ const nextSelected = entries.find((entry) => entry.id === appState.shell.selected_entry_id) || entries[0];
+ if (!nextSelected) {
+ setSelectedDetail(null);
+ renderDetail();
+ return;
+ }
+ if (!selectedEntry() || nextSelected.id !== appState.shell.selected_entry_id) {
+ await loadEntryDetail(nextSelected.id);
+ return;
+ }
+ renderDetail();
+}
+
+async function refreshShell() {
+ const data = await invoke("app_bootstrap");
+ if (!data.logged_in || !data.shell) return;
+ const previousFolder = appState.activeFolder;
+ const previousType = appState.activeType;
+ const previousQuery = appState.query;
+ const previousSelectedId = appState.shell?.selected_entry_id;
+ renderShell(data.shell);
+ appState.activeFolder = previousFolder;
+ appState.activeType = previousType;
+ appState.query = previousQuery;
+ renderFolders();
+ renderTypeFilter();
+ await refreshEntries();
+ if (previousSelectedId && appState.shell.entries.some((entry) => entry.id === previousSelectedId)) {
+ await loadEntryDetail(previousSelectedId);
+ }
+}
+
+function applyUpdatedDetail(detail) {
+ if (!appState.shell) return;
+ const current = appState.shell.entries.find((entry) => entry.id === detail.id);
+ if (current) {
+ current.title = detail.title;
+ current.subtitle = detail.entry_type;
+ current.folder = detail.folder;
+ }
+ setSelectedDetail(detail);
+ renderEntries();
+ renderDetail();
+}
+
+async function openDevices() {
+ if (!invoke) return;
+ const devices = await invoke("device_list");
+ deviceList.innerHTML = devices.length
+ ? devices
+ .map(
+ (device) => `
+
+
${escapeHtml(device.name)}
+
${escapeHtml(device.platform)} · ${escapeHtml(device.client_version)}
+
${escapeHtml(device.last_seen)}
+ ${
+ device.ip
+ ? `
IP · ${escapeHtml(device.ip)}
`
+ : ""
+ }
+
+ `
+ )
+ .join("")
+ : ``;
+ deviceModal.classList.remove("hidden");
+ userMenu.classList.add("hidden");
+}
+
+async function openMcpDialog() {
+ if (!invoke) return;
+ const data = await invoke("mcp_dialog_data");
+ renderMcpDialog(data);
+ mcpModal.classList.remove("hidden");
+}
+
+async function ensureSecretValue(secretId, { visible = false } = {}) {
+ const existing = appState.revealedSecrets[secretId];
+ if (existing) {
+ if (visible) existing.visible = true;
+ return existing;
+ }
+ const revealed = await invoke("reveal_secret_value", { secretId });
+ appState.revealedSecrets[secretId] = { ...revealed, visible };
+ return appState.revealedSecrets[secretId];
+}
+
+async function toggleSecretVisibility(secretId) {
+ const existing = appState.revealedSecrets[secretId];
+ if (existing?.visible) {
+ existing.visible = false;
+ } else {
+ await ensureSecretValue(secretId, { visible: true });
+ }
+ renderDetail();
+}
+
+async function copySecretValue(secretId, button) {
+ const revealed = await ensureSecretValue(secretId);
+ await navigator.clipboard.writeText(revealed.value);
+ setTransientLabel(button?.querySelector(".button-label"), "已复制", "复制");
+}
+
+async function beginEditSecret(secretId) {
+ const detail = selectedEntry();
+ const secret = detail?.secrets.find((item) => item.id === secretId);
+ if (!secret) return;
+ const revealed = await ensureSecretValue(secretId);
+ openSecretModalPanel("edit", {
+ secretId,
+ name: secret.name,
+ secretType: secret.secret_type,
+ value: revealed.value,
+ });
+}
+
+async function openSecretHistory(secretId) {
+ const detail = selectedEntry();
+ const secret = detail?.secrets.find((item) => item.id === secretId);
+ if (!secret) return;
+ const history = await invoke("secret_history", { secretId });
+ appState.historySecretId = secretId;
+ historyModalCopy.textContent = `查看 ${secret.name} 的历史版本并选择回滚目标。`;
+ historyList.innerHTML = history.length
+ ? history
+ .map(
+ (item) => `
+
+
+
v${item.version} · ${escapeHtml(item.action)}
+
${escapeHtml(item.created_at)}
+
+
+ 名称:${escapeHtml(item.name)}
+ 类型:${escapeHtml(item.secret_type)}
+ history_id:${item.history_id}
+
+
${escapeHtml(item.value)}
+
+
+
+
+ `
+ )
+ .join("")
+ : ``;
+ historyModal.classList.remove("hidden");
+}
+
+async function saveSecretModalState() {
+ const detail = selectedEntry();
+ if (!detail) return;
+ const payload = {
+ name: secretNameInput.value.trim(),
+ secretType: secretTypeInput.value || "text",
+ value: secretValueInput.value,
+ };
+ if (!payload.name || !payload.value) {
+ window.alert("请填写完整的密钥名称和内容。");
+ return;
+ }
+ if (appState.secretModalMode === "edit" && appState.editingSecretId) {
+ const updated = await invoke("update_secret", {
+ secret: {
+ id: appState.editingSecretId,
+ name: payload.name,
+ secretType: payload.secretType,
+ value: payload.value,
+ },
+ });
+ closeSecretModalPanel();
+ applyUpdatedDetail(updated);
+ return;
+ }
+ const created = await invoke("create_secret", {
+ entryId: detail.id,
+ secret: payload,
+ });
+ closeSecretModalPanel();
+ applyUpdatedDetail(created);
+}
+
+async function createNewEntry() {
+ const folder = entryModalFolder.value.trim();
+ const title = entryModalTitle.value.trim();
+ const entryType = entryModalType.value.trim() || "service";
+ if (!folder || !title) {
+ window.alert("请填写项目和名称。");
+ return;
+ }
+ const detail = await invoke("create_entry", {
+ entry: {
+ folder,
+ title,
+ entryType,
+ metadata: [],
+ secrets: [],
+ },
+ });
+ closeEntryModalPanel();
+ appState.activeFolder = folder;
+ await refreshShell();
+ await loadEntryDetail(detail.id);
+}
+
+folderList.addEventListener("click", async (event) => {
+ const button = event.target.closest("[data-folder]");
+ if (!button || !appState.shell) return;
+ appState.activeFolder = button.dataset.folder;
+ appState.editing = false;
+ renderFolders();
+ await refreshEntries();
+});
+
+entryList.addEventListener("click", async (event) => {
+ const button = event.target.closest("[data-entry-id]");
+ if (!button) return;
+ const { entryId } = button.dataset;
+ if (!entryId || entryId === appState.shell?.selected_entry_id) return;
+ await loadEntryDetail(entryId);
+});
+
+searchInput.addEventListener("input", async () => {
+ appState.query = searchInput.value;
+ await refreshEntries();
+});
+
+typeFilter.addEventListener("change", async () => {
+ appState.activeType = typeFilter.value;
+ await refreshEntries();
+});
+
+editEntryButton.addEventListener("click", () => {
+ const detail = selectedEntry();
+ if (!detail) return;
+ appState.editing = true;
+ appState.draftMetadata = detail.metadata.map((item) => ({ ...item }));
+ renderDetail();
+});
+
+cancelEditButton.addEventListener("click", () => {
+ appState.editing = false;
+ appState.draftMetadata = selectedEntry()?.metadata.map((item) => ({ ...item })) || [];
+ renderDetail();
+});
+
+metadataEditor.addEventListener("input", (event) => {
+ const target = event.target;
+ const keyIndex = target.dataset.metadataKey;
+ const valueIndex = target.dataset.metadataValue;
+ if (keyIndex !== undefined) {
+ appState.draftMetadata[Number(keyIndex)].label = target.value;
+ }
+ if (valueIndex !== undefined) {
+ appState.draftMetadata[Number(valueIndex)].value = target.value;
+ }
+});
+
+metadataEditor.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-remove-metadata]");
+ if (!button) return;
+ const index = Number(button.dataset.removeMetadata);
+ appState.draftMetadata.splice(index, 1);
+ renderMetadataEditor();
+});
+
+addMetadataButton.addEventListener("click", () => {
+ appState.draftMetadata.push({ label: "", value: "" });
+ renderMetadataEditor();
+});
+
+saveEntryButton.addEventListener("click", async () => {
+ const detail = selectedEntry();
+ if (!detail) return;
+ try {
+ const updated = await invoke("update_entry_detail", {
+ entry: {
+ ...detail,
+ title: nameInput.value.trim(),
+ metadata: appState.draftMetadata.filter((item) => item.label.trim()),
+ },
+ });
+ applyUpdatedDetail(updated);
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+
+deleteEntryButton.addEventListener("click", async () => {
+ if (!appState.shell?.selected_entry_id) return;
+ if (!window.confirm("确定要将当前条目移到最近删除吗?")) return;
+ try {
+ await invoke("delete_entry", { entryId: appState.shell.selected_entry_id });
+ appState.editing = false;
+ await refreshShell();
+ await refreshEntries();
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+
+restoreEntryButton.addEventListener("click", async () => {
+ if (!appState.shell?.selected_entry_id) return;
+ try {
+ await invoke("restore_deleted_entry", { entryId: appState.shell.selected_entry_id });
+ appState.activeFolder = "所有项目";
+ appState.editing = false;
+ await refreshShell();
+ await refreshEntries();
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+
+addSecretButton.addEventListener("click", () => {
+ if (!selectedEntry()) return;
+ openSecretModalPanel("create");
+});
+
+newEntryButton.addEventListener("click", () => {
+ openEntryModalPanel();
+});
+
+loginButton.addEventListener("click", doDemoLogin);
+logoutButton.addEventListener("click", doLogout);
+manageDevicesButton.addEventListener("click", openDevices);
+openMcpModalButton.addEventListener("click", openMcpDialog);
+userTrigger.addEventListener("click", () => userMenu.classList.toggle("hidden"));
+closeDeviceModal.addEventListener("click", () => deviceModal.classList.add("hidden"));
+closeMcpModal.addEventListener("click", () => mcpModal.classList.add("hidden"));
+closeEntryModal.addEventListener("click", closeEntryModalPanel);
+entryModalCancel.addEventListener("click", closeEntryModalPanel);
+entryModalSave.addEventListener("click", async () => {
+ try {
+ await createNewEntry();
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+closeSecretModal.addEventListener("click", closeSecretModalPanel);
+secretModalCancel.addEventListener("click", closeSecretModalPanel);
+secretModalSave.addEventListener("click", async () => {
+ try {
+ await saveSecretModalState();
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+closeHistoryModal.addEventListener("click", closeHistoryModalPanel);
+
+deviceModal.addEventListener("click", (event) => {
+ if (event.target === deviceModal) {
+ deviceModal.classList.add("hidden");
+ }
+});
+mcpModal.addEventListener("click", (event) => {
+ if (event.target === mcpModal) {
+ mcpModal.classList.add("hidden");
+ }
+});
+entryModal.addEventListener("click", (event) => {
+ if (event.target === entryModal) {
+ closeEntryModalPanel();
+ }
+});
+secretModal.addEventListener("click", (event) => {
+ if (event.target === secretModal) {
+ closeSecretModalPanel();
+ }
+});
+historyModal.addEventListener("click", (event) => {
+ if (event.target === historyModal) {
+ closeHistoryModalPanel();
+ }
+});
+
+mcpIntegrationList.addEventListener("click", async (event) => {
+ const button = event.target.closest("[data-apply-target]");
+ if (!button || !invoke) return;
+ button.disabled = true;
+ button.classList.add("is-loading");
+ try {
+ const result = await invoke("apply_mcp_config", {
+ target: button.dataset.applyTarget,
+ });
+ renderMcpDialog(result.data);
+ } finally {
+ button.disabled = false;
+ button.classList.remove("is-loading");
+ }
+});
+
+secretList.addEventListener("click", async (event) => {
+ const button = event.target.closest("[data-secret-action]");
+ if (!button) return;
+ const secretId = button.dataset.secretId;
+ const action = button.dataset.secretAction;
+ if (!secretId) return;
+ try {
+ if (action === "toggle") {
+ await toggleSecretVisibility(secretId);
+ return;
+ }
+ if (action === "copy") {
+ await copySecretValue(secretId, button);
+ return;
+ }
+ if (action === "edit") {
+ await beginEditSecret(secretId);
+ return;
+ }
+ if (action === "history") {
+ await openSecretHistory(secretId);
+ return;
+ }
+ if (action === "delete") {
+ if (!window.confirm("确定要删除这个密钥吗?")) return;
+ await invoke("delete_secret", { secretId });
+ await loadEntryDetail(selectedEntry().id);
+ }
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+
+historyList.addEventListener("click", async (event) => {
+ const button = event.target.closest("[data-history-version]");
+ if (!button || !appState.historySecretId) return;
+ if (!window.confirm(`确定回滚到 v${button.dataset.historyVersion} 吗?`)) return;
+ try {
+ const detail = await invoke("rollback_secret", {
+ secretId: appState.historySecretId,
+ version: Number(button.dataset.historyVersion),
+ historyId: Number(button.dataset.historyId),
+ });
+ closeHistoryModalPanel();
+ applyUpdatedDetail(detail);
+ } catch (error) {
+ window.alert(String(error));
+ }
+});
+
+copyMcpConfigButton.addEventListener("click", async () => {
+ if (!appState.mcpJson) return;
+ await navigator.clipboard.writeText(appState.mcpJson);
+ copyMcpConfigButtonLabel.textContent = "已复制";
+ setTimeout(() => {
+ copyMcpConfigButtonLabel.textContent = "复制";
+ }, 1200);
+});
+
+document.addEventListener("click", (event) => {
+ if (!userTrigger.contains(event.target) && !userMenu.contains(event.target)) {
+ userMenu.classList.add("hidden");
+ }
+});
+
+bootstrap();
diff --git a/apps/desktop/dist/styles.css b/apps/desktop/dist/styles.css
new file mode 100644
index 0000000..e978329
--- /dev/null
+++ b/apps/desktop/dist/styles.css
@@ -0,0 +1,1072 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ color-scheme: dark;
+ --bg-app: #0a0a0b;
+ --bg-panel: #111113;
+ --bg-elevated: #17171a;
+ --bg-hover: #1d1d22;
+ --bg-input: #141418;
+ --border-subtle: #26262c;
+ --border-strong: #34343d;
+ --text-primary: #f5f5f7;
+ --text-secondary: #b3b3bd;
+ --text-tertiary: #7c7c88;
+ --accent-blue: #3b82f6;
+ --accent-blue-hover: #4c8dff;
+ --accent-red: #ef4444;
+ --accent-amber: #d97706;
+ --accent-purple-muted: #2a2440;
+ --accent-purple-text: #a5b4fc;
+ --shadow-panel: 0 16px 32px rgba(0, 0, 0, 0.34);
+ --shadow-modal: 0 24px 48px rgba(0, 0, 0, 0.45);
+ --radius-panel: 20px;
+ --radius-control: 14px;
+}
+
+html,
+body {
+ min-height: 100%;
+ user-select: none;
+ -webkit-user-select: none;
+ -webkit-user-drag: none;
+}
+
+body {
+ margin: 0;
+ font-family: Inter, "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ background: var(--bg-app);
+ color: var(--text-primary);
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+button {
+ color: inherit;
+}
+
+button,
+input,
+select,
+pre {
+ transition:
+ border-color 140ms ease,
+ background-color 140ms ease,
+ color 140ms ease,
+ box-shadow 140ms ease,
+ opacity 140ms ease;
+}
+
+button:focus-visible,
+input:focus-visible,
+select:focus-visible {
+ outline: none;
+ border-color: var(--accent-blue);
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16);
+}
+
+.hidden {
+ display: none !important;
+}
+
+.subtle {
+ color: var(--text-tertiary);
+}
+
+.login-screen {
+ min-height: 100vh;
+ display: grid;
+ grid-template-rows: 38px minmax(0, 1fr);
+ background: var(--bg-panel);
+ position: relative;
+}
+
+.window-titlebar {
+ height: 38px;
+}
+
+.login-titlebar,
+.shell-titlebar {
+ width: 100%;
+}
+
+.login-card {
+ width: min(100%, 420px);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+ margin: auto;
+ padding: 48px 40px 34px;
+ background: transparent;
+ border: none;
+ box-shadow: none;
+}
+
+.login-main {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+}
+
+.login-emblem {
+ width: 56px;
+ height: 56px;
+ display: grid;
+ place-items: center;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border-subtle);
+ border-radius: 999px;
+}
+
+.login-lock-icon {
+ width: 24px;
+ height: 24px;
+ color: var(--text-secondary);
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.login-title-block {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+ text-align: center;
+}
+
+.login-title-block h1,
+.detail-header h2,
+.detail-section h3,
+.modal-header h3,
+.mcp-json-header h4 {
+ margin: 0;
+}
+
+.login-title-block h1 {
+ font-size: 32px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.login-subtle {
+ width: 100%;
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 14px;
+ line-height: 1.45;
+ font-weight: 500;
+}
+
+.login-actions {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+}
+
+.login-google-button {
+ width: 100%;
+ min-height: 40px;
+ padding: 10px 16px;
+ justify-content: center;
+ gap: 10px;
+ border: 1px solid var(--accent-blue);
+ background: var(--accent-blue);
+}
+
+.login-google-mark {
+ width: 18px;
+ height: 18px;
+ color: #f5f5f7;
+ flex: none;
+}
+
+.error-text {
+ width: 100%;
+ margin: 0;
+ color: var(--accent-red);
+ font-size: 13px;
+ text-align: center;
+}
+
+.shell {
+ display: grid;
+ grid-template-columns: 248px minmax(0, 1fr);
+ grid-template-rows: 38px minmax(0, 1fr);
+ min-height: 100vh;
+ background: var(--bg-app);
+}
+
+.shell-titlebar {
+ grid-column: 1 / -1;
+ grid-row: 1;
+}
+
+.sidebar {
+ grid-column: 1;
+ grid-row: 2;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 20px 16px;
+ background: var(--bg-panel);
+ border-right: 1px solid var(--border-subtle);
+}
+
+.sidebar-spacer {
+ flex: 1;
+}
+
+.user-block {
+ position: relative;
+}
+
+.user-trigger {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 16px;
+ background: var(--bg-elevated);
+ color: var(--text-primary);
+ text-align: left;
+ cursor: pointer;
+}
+
+.avatar {
+ width: 36px;
+ height: 36px;
+ display: grid;
+ place-items: center;
+ border-radius: 999px;
+ background: #d97706;
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 700;
+}
+
+.user-copy {
+ min-width: 0;
+ flex: 1;
+}
+
+.user-name {
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.user-email {
+ color: var(--text-secondary);
+ font-size: 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.caret {
+ color: var(--text-tertiary);
+}
+
+.user-menu {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0;
+ width: 100%;
+ padding: 6px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border-subtle);
+ border-radius: 14px;
+ box-shadow: var(--shadow-panel);
+ z-index: 20;
+}
+
+.menu-item {
+ width: 100%;
+ padding: 10px 12px;
+ border: none;
+ border-radius: 10px;
+ background: transparent;
+ color: var(--text-primary);
+ text-align: left;
+ cursor: pointer;
+}
+
+.menu-item:hover {
+ background: var(--bg-hover);
+}
+
+.menu-item.danger {
+ color: #f87171;
+}
+
+.folder-list {
+ display: grid;
+ gap: 10px;
+}
+
+.folder-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 12px 14px;
+ border: 1px solid transparent;
+ border-radius: 14px;
+ background: transparent;
+ color: var(--text-secondary);
+ text-align: left;
+ cursor: pointer;
+}
+
+.folder-item:hover {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.folder-item.active {
+ background: var(--bg-elevated);
+ border-color: var(--border-strong);
+ color: var(--accent-purple-text);
+ font-weight: 600;
+}
+
+.folder-item.active .folder-count {
+ background: var(--accent-purple-muted);
+ color: var(--accent-purple-text);
+}
+
+.folder-count {
+ min-width: 24px;
+ padding: 4px 9px;
+ border-radius: 999px;
+ background: var(--bg-hover);
+ color: var(--text-secondary);
+ font-size: 12px;
+ text-align: center;
+}
+
+.sidebar-footer {
+ display: grid;
+ gap: 8px;
+}
+
+.sidebar-utility {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 12px;
+ background: var(--bg-input);
+ color: var(--text-secondary);
+ text-align: left;
+ cursor: pointer;
+}
+
+.sidebar-utility:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.sidebar-utility-icon {
+ width: 18px;
+ display: inline-flex;
+ justify-content: center;
+ color: var(--text-secondary);
+}
+
+.main-shell {
+ grid-column: 2;
+ grid-row: 2;
+ display: grid;
+ grid-template-columns: 404px minmax(0, 1fr);
+ min-width: 0;
+ min-height: 0;
+}
+
+.list-column {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ background: var(--bg-panel);
+ border-right: 1px solid var(--border-subtle);
+}
+
+.searchbar-shell,
+.list-pane,
+.toolbar {
+ background: var(--bg-panel);
+}
+
+.searchbar-shell {
+ padding: 18px 18px 14px;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.global-search {
+ max-width: none;
+}
+
+.search-input,
+.filter-select,
+.detail-input {
+ width: 100%;
+ padding: 12px 14px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 14px;
+ background: var(--bg-input);
+ color: var(--text-primary);
+ outline: none;
+}
+
+.search-input::placeholder,
+.detail-input::placeholder {
+ color: var(--text-tertiary);
+}
+
+.filter-select {
+ appearance: none;
+}
+
+.list-pane {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ flex: 1;
+}
+
+.toolbar {
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.entry-list {
+ flex: 1;
+ padding: 14px 18px 18px;
+ display: grid;
+ gap: 10px;
+ align-content: start;
+ overflow: auto;
+}
+
+.entry-item {
+ width: 100%;
+ padding: 16px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 16px;
+ background: var(--bg-input);
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.entry-item:hover {
+ background: var(--bg-hover);
+}
+
+.entry-item.active {
+ background: var(--bg-elevated);
+ border-color: var(--border-strong);
+}
+
+.entry-title {
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.entry-subtitle,
+.entry-folder-chip,
+.integration-path {
+ margin-top: 4px;
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.detail-pane {
+ min-width: 0;
+ overflow: auto;
+ padding: 36px;
+ background: var(--bg-app);
+}
+
+.detail-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ margin-bottom: 32px;
+}
+
+.detail-title-stack {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.detail-folder-label {
+ color: var(--text-tertiary);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.detail-title-block {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.detail-header h2 {
+ font-size: 32px;
+ font-weight: 700;
+}
+
+.detail-badge {
+ padding: 4px 8px;
+ border-radius: 999px;
+ background: #422006;
+ color: #fbbf24;
+ font-size: 12px;
+}
+
+.detail-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.primary,
+.secondary-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-radius: 12px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.primary {
+ border: none;
+ background: var(--accent-blue);
+ color: var(--text-primary);
+}
+
+.primary:hover {
+ background: var(--accent-blue-hover);
+}
+
+.secondary-button {
+ border: 1px solid var(--border-strong);
+ background: var(--bg-elevated);
+ color: var(--text-primary);
+}
+
+.secondary-button:hover {
+ background: var(--bg-hover);
+}
+
+.secondary-button.small,
+.primary.small {
+ padding: 10px 14px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.secondary-button.large {
+ padding: 14px 28px;
+}
+
+.secondary-button.danger {
+ border-color: #4b2529;
+ background: #2b1316;
+ color: #fca5a5;
+}
+
+.secondary-button:disabled,
+.primary:disabled,
+.integration-toggle:disabled,
+.secret-inline-action:disabled {
+ cursor: default;
+ opacity: 0.7;
+}
+
+.detail-section {
+ margin-bottom: 18px;
+}
+
+.detail-section h3 {
+ margin-bottom: 18px;
+ color: var(--text-tertiary);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.detail-edit-section {
+ max-width: 420px;
+}
+
+.detail-inline-value {
+ color: var(--text-primary);
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.detail-section.compact {
+ margin-bottom: 0;
+}
+
+.detail-fields,
+.device-list {
+ display: grid;
+ gap: 16px;
+}
+
+.detail-fields {
+ grid-template-columns: repeat(2, minmax(220px, 1fr));
+}
+
+.detail-field {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 16px 18px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 16px;
+ background: var(--bg-panel);
+}
+
+.detail-label,
+.secret-type {
+ color: var(--text-tertiary);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.detail-value {
+ color: var(--text-primary);
+ font-size: 15px;
+ font-weight: 600;
+ word-break: break-word;
+}
+
+.metadata-editor {
+ display: grid;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.metadata-row {
+ display: grid;
+ grid-template-columns: minmax(140px, 180px) minmax(0, 1fr) auto;
+ gap: 10px;
+}
+
+.metadata-row .detail-input {
+ min-width: 0;
+}
+
+.secret-list {
+ display: grid;
+ gap: 18px;
+}
+
+.secret-card,
+.device-card {
+ padding: 20px 22px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 20px;
+ background: var(--bg-elevated);
+}
+
+.secret-row {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ margin-top: 12px;
+}
+
+.secret-shell {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ padding: 14px 16px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 14px;
+ background: var(--bg-input);
+}
+
+.secret-leading-icon,
+.secret-inline-action {
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.secret-inline-action {
+ border: none;
+ background: transparent;
+}
+
+.secret-value {
+ flex: 1;
+ min-width: 0;
+ color: var(--text-secondary);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ text-align: left;
+}
+
+.secret-copy-button {
+ flex: none;
+}
+
+.secret-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.action-button {
+ min-height: 40px;
+ padding-inline: 16px;
+}
+
+.action-button .button-label {
+ color: inherit;
+}
+
+.modal {
+ position: fixed;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+ background: rgba(5, 5, 7, 0.72);
+}
+
+.modal-card {
+ width: min(560px, 100%);
+ max-height: calc(100vh - 48px);
+ overflow: auto;
+ padding: 28px;
+ background: var(--bg-panel);
+ border: 1px solid var(--border-subtle);
+ border-radius: 24px;
+ box-shadow: var(--shadow-modal);
+}
+
+.modal-card.wide {
+ width: min(960px, 100%);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.modal-header h3 {
+ font-size: 22px;
+ font-weight: 700;
+}
+
+.icon-button {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--border-strong);
+ border-radius: 999px;
+ background: var(--bg-elevated);
+ color: var(--text-primary);
+ cursor: pointer;
+}
+
+.modal-copy {
+ margin: 0 0 20px;
+ color: var(--text-secondary);
+ font-size: 13px;
+ line-height: 1.45;
+}
+
+.modal-section {
+ margin-bottom: 20px;
+}
+
+.modal-footnote {
+ margin: 14px 0 0;
+ color: var(--text-tertiary);
+ font-size: 11px;
+ line-height: 1.45;
+}
+
+.modal-form {
+ display: grid;
+ gap: 14px;
+}
+
+.field-label {
+ display: grid;
+ gap: 8px;
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.detail-textarea {
+ min-height: 128px;
+ resize: vertical;
+}
+
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 22px;
+}
+
+.section-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+}
+
+.history-list {
+ display: grid;
+ gap: 14px;
+}
+
+.history-item {
+ padding: 18px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 18px;
+ background: var(--bg-elevated);
+}
+
+.history-topline,
+.history-meta,
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.history-topline,
+.toolbar {
+ justify-content: space-between;
+}
+
+.history-meta {
+ margin-top: 10px;
+ flex-wrap: wrap;
+ color: var(--text-tertiary);
+ font-size: 12px;
+}
+
+.history-value {
+ margin-top: 14px;
+ padding: 14px 16px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 14px;
+ background: var(--bg-input);
+ color: var(--text-secondary);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 12px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.history-actions {
+ margin-top: 14px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.integration-list {
+ display: grid;
+ gap: 0;
+ border: 1px solid var(--border-subtle);
+ border-radius: 18px;
+ overflow: hidden;
+ background: var(--bg-panel);
+}
+
+.integration-row-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 14px 20px;
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.integration-row-card:last-child {
+ border-bottom: none;
+}
+
+.integration-app {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 0;
+}
+
+.integration-icon {
+ width: 34px;
+ height: 34px;
+ display: grid;
+ place-items: center;
+ border-radius: 10px;
+ background: var(--bg-elevated);
+ color: var(--text-secondary);
+ font-size: 13px;
+}
+
+.integration-path {
+ margin-top: 4px;
+ font-size: 12px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ word-break: break-all;
+}
+
+.integration-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-end;
+ width: 40px;
+ height: 22px;
+ padding: 3px;
+ border: 1px solid transparent;
+ border-radius: 999px;
+ background: var(--bg-hover);
+ cursor: pointer;
+}
+
+.integration-toggle.is-on {
+ background: var(--accent-blue);
+ border-color: var(--accent-blue-hover);
+}
+
+.integration-toggle-knob {
+ width: 16px;
+ height: 16px;
+ border-radius: 999px;
+ background: var(--bg-panel);
+ border: 1px solid var(--bg-app);
+}
+
+.integration-toggle:not(.is-on) .integration-toggle-knob {
+ margin-right: auto;
+}
+
+.integration-toggle.is-loading {
+ opacity: 0.7;
+}
+
+.mcp-json-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.mcp-json-header h4 {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.mcp-config {
+ margin: 0;
+ padding: 18px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 16px;
+ background: var(--bg-input);
+ color: #cbd5e1;
+ overflow: auto;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.button-icon {
+ font-size: 13px;
+ color: var(--text-secondary);
+ line-height: 1;
+}
+
+.primary .button-icon {
+ color: var(--text-primary);
+}
+
+.device-list {
+ gap: 12px;
+}
+
+.empty-card {
+ padding: 18px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 16px;
+ background: var(--bg-panel);
+}
+
+.empty-title {
+ color: var(--text-primary);
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.empty-copy {
+ color: var(--text-secondary);
+ font-size: 13px;
+ line-height: 1.45;
+}
+
+@media (max-width: 1180px) {
+ .main-shell {
+ grid-template-columns: 340px minmax(0, 1fr);
+ }
+
+ .detail-fields {
+ grid-template-columns: minmax(0, 1fr);
+ }
+}
+
+@media (max-width: 960px) {
+ .shell {
+ grid-template-columns: 220px minmax(0, 1fr);
+ }
+
+ .main-shell {
+ grid-template-columns: minmax(280px, 320px) minmax(0, 1fr);
+ }
+
+ .detail-pane {
+ padding: 28px 20px;
+ }
+}