diff --git a/.gitignore b/.gitignore index 4fbdb4c1..e9bba417 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ win-unpacked .blockmap .claude build +.omc # Test artifacts test-results/ diff --git a/build/release/builder-debug.yml b/build/release/builder-debug.yml index 5027d739..1121a82e 100644 --- a/build/release/builder-debug.yml +++ b/build/release/builder-debug.yml @@ -1,25 +1,3 @@ -x64: - firstOrDefaultFilePatterns: - - '!**/node_modules/**' - - '!assets{,/**/*}' - - '!build/release{,/**/*}' - - out/** - - package.json - - assets/icon.png - - assets/tray-icon*.png - - '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}' - - '!**/._*' - - '!**/electron-builder.{yaml,yml,json,json5,toml,ts}' - - '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}' - - '!.yarn{,/**/*}' - - '!.editorconfig' - - '!.yarnrc.yml' - nodeModuleFilePatterns: - - '**/*' - - out/** - - package.json - - assets/icon.png - - assets/tray-icon*.png arm64: firstOrDefaultFilePatterns: - '!**/node_modules/**' @@ -42,5 +20,27 @@ arm64: - package.json - assets/icon.png - assets/tray-icon*.png +x64: + firstOrDefaultFilePatterns: + - '!**/node_modules/**' + - '!assets{,/**/*}' + - '!build/release{,/**/*}' + - out/** + - package.json + - assets/icon.png + - assets/tray-icon*.png + - '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}' + - '!**/._*' + - '!**/electron-builder.{yaml,yml,json,json5,toml,ts}' + - '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}' + - '!.yarn{,/**/*}' + - '!.editorconfig' + - '!.yarnrc.yml' + nodeModuleFilePatterns: + - '**/*' + - out/** + - package.json + - assets/icon.png + - assets/tray-icon*.png nsis: - script: "!include \"C:\\Users\\zhang\\Documents\\GitHub\\AgentCompany\\node_modules\\.pnpm\\app-builder-lib@26.8.1_dmg-_62d31da0b96a3ea7b7d51a6bbe3b3b6e\\node_modules\\app-builder-lib\\templates\\nsis\\include\\StdUtils.nsh\"\n!addincludedir \"C:\\Users\\zhang\\Documents\\GitHub\\AgentCompany\\node_modules\\.pnpm\\app-builder-lib@26.8.1_dmg-_62d31da0b96a3ea7b7d51a6bbe3b3b6e\\node_modules\\app-builder-lib\\templates\\nsis\\include\"\n!macro _isUpdated _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"updated\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isUpdated `\"\" isUpdated \"\"`\n\n!macro _isForceRun _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"force-run\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForceRun `\"\" isForceRun \"\"`\n\n!macro _isKeepShortcuts _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"keep-shortcuts\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isKeepShortcuts `\"\" isKeepShortcuts \"\"`\n\n!macro _isNoDesktopShortcut _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"no-desktop-shortcut\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isNoDesktopShortcut `\"\" isNoDesktopShortcut \"\"`\n\n!macro _isDeleteAppData _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"delete-app-data\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isDeleteAppData `\"\" isDeleteAppData \"\"`\n\n!macro _isForAllUsers _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"allusers\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForAllUsers `\"\" isForAllUsers \"\"`\n\n!macro _isForCurrentUser _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"currentuser\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForCurrentUser `\"\" isForCurrentUser \"\"`\n\n!macro addLangs\n !insertmacro MUI_LANGUAGE \"English\"\n !insertmacro MUI_LANGUAGE \"German\"\n !insertmacro MUI_LANGUAGE \"French\"\n !insertmacro MUI_LANGUAGE \"SpanishInternational\"\n !insertmacro MUI_LANGUAGE \"SimpChinese\"\n !insertmacro MUI_LANGUAGE \"TradChinese\"\n !insertmacro MUI_LANGUAGE \"Japanese\"\n !insertmacro MUI_LANGUAGE \"Korean\"\n !insertmacro MUI_LANGUAGE \"Italian\"\n !insertmacro MUI_LANGUAGE \"Dutch\"\n !insertmacro MUI_LANGUAGE \"Danish\"\n !insertmacro MUI_LANGUAGE \"Swedish\"\n !insertmacro MUI_LANGUAGE \"Norwegian\"\n !insertmacro MUI_LANGUAGE \"Finnish\"\n !insertmacro MUI_LANGUAGE \"Russian\"\n !insertmacro MUI_LANGUAGE \"Portuguese\"\n !insertmacro MUI_LANGUAGE \"PortugueseBR\"\n !insertmacro MUI_LANGUAGE \"Polish\"\n !insertmacro MUI_LANGUAGE \"Ukrainian\"\n !insertmacro MUI_LANGUAGE \"Czech\"\n !insertmacro MUI_LANGUAGE \"Slovak\"\n !insertmacro MUI_LANGUAGE \"Hungarian\"\n !insertmacro MUI_LANGUAGE \"Arabic\"\n !insertmacro MUI_LANGUAGE \"Turkish\"\n !insertmacro MUI_LANGUAGE \"Thai\"\n !insertmacro MUI_LANGUAGE \"Vietnamese\"\n!macroend\n\n!addplugindir /x86-unicode \"C:\\Users\\zhang\\AppData\\Local\\electron-builder\\Cache\\nsis\\nsis-resources-3.4.1-nsis-resources-3.4.1\\plugins\\x86-unicode\"\n!include \"C:\\Users\\zhang\\AppData\\Local\\Temp\\t-GU2sm0\\6-messages.nsh\"\n\n!include \"common.nsh\"\n!include \"extractAppPackage.nsh\"\n\n# https://github.com/electron-userland/electron-builder/issues/3972#issuecomment-505171582\nCRCCheck off\nWindowIcon Off\nAutoCloseWindow True\nRequestExecutionLevel ${REQUEST_EXECUTION_LEVEL}\n\nFunction .onInit\n !ifndef SPLASH_IMAGE\n SetSilent silent\n !endif\n\n !insertmacro check64BitAndSetRegView\nFunctionEnd\n\nFunction .onGUIInit\n InitPluginsDir\n\n !ifdef SPLASH_IMAGE\n File /oname=$PLUGINSDIR\\splash.bmp \"${SPLASH_IMAGE}\"\n BgImage::SetBg $PLUGINSDIR\\splash.bmp\n BgImage::Redraw\n !endif\nFunctionEnd\n\nSection\n !ifdef SPLASH_IMAGE\n HideWindow\n !endif\n\n StrCpy $INSTDIR \"$PLUGINSDIR\\app\"\n !ifdef UNPACK_DIR_NAME\n StrCpy $INSTDIR \"$TEMP\\${UNPACK_DIR_NAME}\"\n !endif\n\n RMDir /r $INSTDIR\n SetOutPath $INSTDIR\n\n !ifdef APP_DIR_64\n !ifdef APP_DIR_ARM64\n !ifdef APP_DIR_32\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${elseif} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_64}\\*.*\"\n {endIf}\n !endif\n !else\n !ifdef APP_DIR_32\n ${if} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n File /r \"${APP_DIR_64}\\*.*\"\n !endif\n !endif\n !else\n !ifdef APP_DIR_32\n File /r \"${APP_DIR_32}\\*.*\"\n !else\n !insertmacro extractEmbeddedAppPackage\n !endif\n !endif\n\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_DIR\", \"$EXEDIR\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_FILE\", \"$EXEPATH\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_APP_FILENAME\", \"${APP_FILENAME}\").r0'\n ${StdUtils.GetAllParameters} $R0 0\n\n !ifdef SPLASH_IMAGE\n BgImage::Destroy\n !endif\n\n\tExecWait \"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $R0\" $0\n SetErrorLevel $0\n\n SetOutPath $EXEDIR\n\tRMDir /r $INSTDIR\nSectionEnd\n" + script: "!include \"C:\\Users\\zhang\\Documents\\GitHub\\AgentCompany\\node_modules\\.pnpm\\app-builder-lib@26.8.1_dmg-_62d31da0b96a3ea7b7d51a6bbe3b3b6e\\node_modules\\app-builder-lib\\templates\\nsis\\include\\StdUtils.nsh\"\n!addincludedir \"C:\\Users\\zhang\\Documents\\GitHub\\AgentCompany\\node_modules\\.pnpm\\app-builder-lib@26.8.1_dmg-_62d31da0b96a3ea7b7d51a6bbe3b3b6e\\node_modules\\app-builder-lib\\templates\\nsis\\include\"\n!macro _isUpdated _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"updated\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isUpdated `\"\" isUpdated \"\"`\n\n!macro _isForceRun _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"force-run\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForceRun `\"\" isForceRun \"\"`\n\n!macro _isKeepShortcuts _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"keep-shortcuts\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isKeepShortcuts `\"\" isKeepShortcuts \"\"`\n\n!macro _isNoDesktopShortcut _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"no-desktop-shortcut\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isNoDesktopShortcut `\"\" isNoDesktopShortcut \"\"`\n\n!macro _isDeleteAppData _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"delete-app-data\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isDeleteAppData `\"\" isDeleteAppData \"\"`\n\n!macro _isForAllUsers _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"allusers\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForAllUsers `\"\" isForAllUsers \"\"`\n\n!macro _isForCurrentUser _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"currentuser\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForCurrentUser `\"\" isForCurrentUser \"\"`\n\n!macro addLangs\n !insertmacro MUI_LANGUAGE \"English\"\n !insertmacro MUI_LANGUAGE \"German\"\n !insertmacro MUI_LANGUAGE \"French\"\n !insertmacro MUI_LANGUAGE \"SpanishInternational\"\n !insertmacro MUI_LANGUAGE \"SimpChinese\"\n !insertmacro MUI_LANGUAGE \"TradChinese\"\n !insertmacro MUI_LANGUAGE \"Japanese\"\n !insertmacro MUI_LANGUAGE \"Korean\"\n !insertmacro MUI_LANGUAGE \"Italian\"\n !insertmacro MUI_LANGUAGE \"Dutch\"\n !insertmacro MUI_LANGUAGE \"Danish\"\n !insertmacro MUI_LANGUAGE \"Swedish\"\n !insertmacro MUI_LANGUAGE \"Norwegian\"\n !insertmacro MUI_LANGUAGE \"Finnish\"\n !insertmacro MUI_LANGUAGE \"Russian\"\n !insertmacro MUI_LANGUAGE \"Portuguese\"\n !insertmacro MUI_LANGUAGE \"PortugueseBR\"\n !insertmacro MUI_LANGUAGE \"Polish\"\n !insertmacro MUI_LANGUAGE \"Ukrainian\"\n !insertmacro MUI_LANGUAGE \"Czech\"\n !insertmacro MUI_LANGUAGE \"Slovak\"\n !insertmacro MUI_LANGUAGE \"Hungarian\"\n !insertmacro MUI_LANGUAGE \"Arabic\"\n !insertmacro MUI_LANGUAGE \"Turkish\"\n !insertmacro MUI_LANGUAGE \"Thai\"\n !insertmacro MUI_LANGUAGE \"Vietnamese\"\n!macroend\n\n!addplugindir /x86-unicode \"C:\\Users\\zhang\\AppData\\Local\\electron-builder\\Cache\\nsis\\nsis-resources-3.4.1-nsis-resources-3.4.1\\plugins\\x86-unicode\"\n!include \"C:\\Users\\zhang\\AppData\\Local\\Temp\\t-i79qg1\\6-messages.nsh\"\n\n!include \"common.nsh\"\n!include \"extractAppPackage.nsh\"\n\n# https://github.com/electron-userland/electron-builder/issues/3972#issuecomment-505171582\nCRCCheck off\nWindowIcon Off\nAutoCloseWindow True\nRequestExecutionLevel ${REQUEST_EXECUTION_LEVEL}\n\nFunction .onInit\n !ifndef SPLASH_IMAGE\n SetSilent silent\n !endif\n\n !insertmacro check64BitAndSetRegView\nFunctionEnd\n\nFunction .onGUIInit\n InitPluginsDir\n\n !ifdef SPLASH_IMAGE\n File /oname=$PLUGINSDIR\\splash.bmp \"${SPLASH_IMAGE}\"\n BgImage::SetBg $PLUGINSDIR\\splash.bmp\n BgImage::Redraw\n !endif\nFunctionEnd\n\nSection\n !ifdef SPLASH_IMAGE\n HideWindow\n !endif\n\n StrCpy $INSTDIR \"$PLUGINSDIR\\app\"\n !ifdef UNPACK_DIR_NAME\n StrCpy $INSTDIR \"$TEMP\\${UNPACK_DIR_NAME}\"\n !endif\n\n RMDir /r $INSTDIR\n SetOutPath $INSTDIR\n\n !ifdef APP_DIR_64\n !ifdef APP_DIR_ARM64\n !ifdef APP_DIR_32\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${elseif} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_64}\\*.*\"\n {endIf}\n !endif\n !else\n !ifdef APP_DIR_32\n ${if} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n File /r \"${APP_DIR_64}\\*.*\"\n !endif\n !endif\n !else\n !ifdef APP_DIR_32\n File /r \"${APP_DIR_32}\\*.*\"\n !else\n !insertmacro extractEmbeddedAppPackage\n !endif\n !endif\n\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_DIR\", \"$EXEDIR\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_FILE\", \"$EXEPATH\").r0'\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_APP_FILENAME\", \"${APP_FILENAME}\").r0'\n ${StdUtils.GetAllParameters} $R0 0\n\n !ifdef SPLASH_IMAGE\n BgImage::Destroy\n !endif\n\n\tExecWait \"$INSTDIR\\${APP_EXECUTABLE_FILENAME} $R0\" $0\n SetErrorLevel $0\n\n SetOutPath $EXEDIR\n\tRMDir /r $INSTDIR\nSectionEnd\n" diff --git a/build/release/win-arm64-unpacked/resources/app.asar b/build/release/win-arm64-unpacked/resources/app.asar index 4a3fd83e..2bf206ab 100644 Binary files a/build/release/win-arm64-unpacked/resources/app.asar and b/build/release/win-arm64-unpacked/resources/app.asar differ diff --git a/package.json b/package.json index 007b2a5a..44fed1c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-company", - "version": "0.4.133", + "version": "0.4.135", "private": true, "type": "module", "description": "Cross-platform Electron desktop control plane for local-first AI agent companies.", diff --git a/src/main/database.ts b/src/main/database.ts index 4e4622e8..a96f25b8 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -1955,6 +1955,24 @@ export class AppDatabase { this.setSetting(`budget_reset_month_${companyId}`, month); } + getDisabledModels(connectorId: string): Array<{ id: string; label: string }> { + const raw = this.getSetting(`disabled_models:${connectorId}`); + if (!raw) return []; + try { + return JSON.parse(raw); + } catch { + return raw.split(",").filter(Boolean).map((id) => ({ id, label: id })); + } + } + + setDisabledModels(connectorId: string, models: Array<{ id: string; label: string }>) { + if (models.length === 0) { + this.db.prepare("delete from settings where key = ?").run(`disabled_models:${connectorId}`); + } else { + this.setSetting(`disabled_models:${connectorId}`, JSON.stringify(models)); + } + } + listHeartbeatEligibleAgents(companyId: string): AgentRecord[] { return this.readAll( "select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where company_id = ? and heartbeat_enabled = 1 and status not in ('terminated', 'paused', 'error', 'pending_approval')", @@ -2351,7 +2369,8 @@ export class AppDatabase { private seedConnectors() { // Remove connector rows for adapters that have been removed from the codebase. - this.db.prepare("delete from connectors where id not in ('codex_local', 'claude_local', 'gemini_local')").run(); + // Preserve native connectors and all claude_compat slots (user-configured API providers). + this.db.prepare("delete from connectors where id not in ('codex_local', 'claude_local', 'gemini_local') and id not like 'claude_compat%'").run(); const currentRows = this.readAll>( "select id, label, description, status, command, version, auth_state as authState, last_checked_at as lastCheckedAt, last_error as lastError, model, env_binding_text as envBindingText, notes from connectors", ); diff --git a/src/main/ipc-core-admin-handlers.ts b/src/main/ipc-core-admin-handlers.ts index 47553893..a58f9618 100644 --- a/src/main/ipc-core-admin-handlers.ts +++ b/src/main/ipc-core-admin-handlers.ts @@ -2,6 +2,7 @@ import { execSync } from "node:child_process"; import { IPC_CHANNELS, companyInputSchema, + connectorDisabledModelsSchema, connectorInputSchema, deleteEntitySchema, openTerminalCommandSchema, @@ -98,6 +99,13 @@ export function registerCoreAdminHandlers({ return ok(true); }); + registerHandle(IPC_CHANNELS.setConnectorDisabledModels, async (_event, payload) => { + const parsed = connectorDisabledModelsSchema.parse(payload); + db.setDisabledModels(parsed.connectorId, parsed.disabledModels); + publishDomainChanged(); + return ok(true); + }); + registerHandle(IPC_CHANNELS.saveSecret, async (_event, payload) => { const parsed = secretInputSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); diff --git a/src/main/ipc-core-read-handlers.ts b/src/main/ipc-core-read-handlers.ts index 14e2c367..defc4044 100644 --- a/src/main/ipc-core-read-handlers.ts +++ b/src/main/ipc-core-read-handlers.ts @@ -109,6 +109,15 @@ export function registerCoreReadHandlers({ } }); + registerHandle(IPC_CHANNELS.getConnectorDisabledModels, (_event, connectorId) => { + try { + const parsed = connectorInputSchema.shape.id.parse(connectorId); + return ok(db.getDisabledModels(parsed)); + } catch { + return ok([]); + } + }); + registerHandle(IPC_CHANNELS.getInbox, (_event, payload) => { const companyId = z.string().uuid().parse(payload); const err = ensureCompanyExists(companyId); diff --git a/src/main/main-runtime-support.ts b/src/main/main-runtime-support.ts index 84f7de29..ab845560 100644 --- a/src/main/main-runtime-support.ts +++ b/src/main/main-runtime-support.ts @@ -148,15 +148,19 @@ export function createMainRuntimeSupport(state: MainRuntimeState) { getApiPort: () => state.apiServer?.getPort() ?? 0, notify: (options) => requireIpcSupport().notify(options), listConnectorModels: async (connectorId) => { + const disabled = new Set(requireDb().getDisabledModels(connectorId).map((m) => m.id)); // For claude_compatible, return user-configured models from the DB // (stored as comma-separated values in the connector's model field) if (connectorId.startsWith("claude_compat")) { const connector = requireDb().getConnector(connectorId); const raw = connector.model ?? ""; - return raw.split(",").map((m) => m.trim()).filter(Boolean).map((m) => ({ id: m, label: m })); + return raw.split(",").map((m) => m.trim()).filter(Boolean) + .filter((m) => !disabled.has(m)) + .map((m) => ({ id: m, label: m })); } const definition = getConnectorDefinition(connectorId as Parameters[0]); - return definition.listModels ? definition.listModels() : []; + const models = definition.listModels ? await definition.listModels() : []; + return models.filter((m) => !disabled.has(m.id)); }, }); diff --git a/src/preload/index.ts b/src/preload/index.ts index 042a149c..05fa8d81 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -40,6 +40,8 @@ const api: DesktopApi = { deleteSecret: (input) => ipcRenderer.invoke(IPC_CHANNELS.deleteSecret, input), getSidebarBadges: (companyId) => ipcRenderer.invoke(IPC_CHANNELS.getSidebarBadges, companyId), listConnectorModels: (connectorId) => ipcRenderer.invoke(IPC_CHANNELS.listConnectorModels, connectorId), + getConnectorDisabledModels: (connectorId) => ipcRenderer.invoke(IPC_CHANNELS.getConnectorDisabledModels, connectorId), + setConnectorDisabledModels: (input) => ipcRenderer.invoke(IPC_CHANNELS.setConnectorDisabledModels, input), openConnectorAuthTerminal: (connectorId) => ipcRenderer.invoke(IPC_CHANNELS.openConnectorAuthTerminal, connectorId), addComment: (input) => ipcRenderer.invoke(IPC_CHANNELS.addComment, input), listComments: (input) => ipcRenderer.invoke(IPC_CHANNELS.listComments, input), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 48924b61..0a0f5ee4 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { CaretUpDown, @@ -519,7 +519,7 @@ export function App() { const companyKnowledge = useMemo(() => snapshot?.knowledgeBase?.filter((k) => !currentCompany || k.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]); const companyMeetings = useMemo(() => snapshot?.meetings?.filter((m) => !currentCompany || m.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]); const companySprints = useMemo(() => snapshot?.sprints?.filter((s) => !currentCompany || s.companyId === currentCompany.id) ?? [], [snapshot, currentCompany]); - const paletteQuery = commandPaletteSearch.trim().toLowerCase(); + const paletteQuery = useDeferredValue(commandPaletteSearch.trim().toLowerCase()); const paletteResults = useMemo(() => { const filteredNavItems = paletteQuery ? navItems.filter((item) => item.label.toLowerCase().includes(paletteQuery) || item.id.toLowerCase().includes(paletteQuery)) @@ -581,6 +581,19 @@ export function App() { } }, [runs, tasks]); + // Global Cmd/Ctrl+K to open command palette + useEffect(() => { + function onGlobalKey(event: KeyboardEvent) { + if ((event.metaKey || event.ctrlKey) && event.key === "k") { + event.preventDefault(); + setCommandPaletteOpen((prev) => !prev); + if (!commandPaletteOpen) { setCommandPaletteSearch(""); setSelectedPaletteIndex(0); } + } + } + document.addEventListener("keydown", onGlobalKey); + return () => document.removeEventListener("keydown", onGlobalKey); + }, [commandPaletteOpen]); + useEffect(() => { if (!commandPaletteOpen) return; function onKey(event: KeyboardEvent) { @@ -741,13 +754,13 @@ export function App() {
{showCustomTitleBar && } -
+
)} @@ -899,7 +913,7 @@ export function App() {
{error ? ( -
+
{error}
diff --git a/src/renderer/components/CompanyOnboarding/ClaudeSettingsDialog.tsx b/src/renderer/components/CompanyOnboarding/ClaudeSettingsDialog.tsx index d55d2fa6..2b743754 100644 --- a/src/renderer/components/CompanyOnboarding/ClaudeSettingsDialog.tsx +++ b/src/renderer/components/CompanyOnboarding/ClaudeSettingsDialog.tsx @@ -1,5 +1,7 @@ +import { useEffect } from "react"; import { X } from "@phosphor-icons/react"; import type { AppLocale } from "@shared/locale"; +import { useFocusTrap } from "../ui"; interface ClaudeSettingsDialogProps { claudeApiKey: string; @@ -26,17 +28,20 @@ export function ClaudeSettingsDialog({ saving, currentLocale, }: ClaudeSettingsDialogProps) { + const trapRef = useFocusTrap(true); + useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey) }, [onClose]); return (
-
e.stopPropagation()}> +
e.stopPropagation()}>

Claude Code {currentLocale === "zh" ? "设置" : "Settings"}

- + {currentLocale === "zh" ? "留空使用订阅认证" : "Leave empty to use subscription auth"}
- + setClaudeBaseUrl(e.target.value)} @@ -56,8 +62,9 @@ export function ClaudeSettingsDialog({
{currentLocale === "zh" ? "留空使用默认端点" : "Leave empty for default endpoint"}
- + setClaudeModel(e.target.value)} diff --git a/src/renderer/components/CompanyOnboarding/CompanyStep.tsx b/src/renderer/components/CompanyOnboarding/CompanyStep.tsx index 5b3f331f..51dbe920 100644 --- a/src/renderer/components/CompanyOnboarding/CompanyStep.tsx +++ b/src/renderer/components/CompanyOnboarding/CompanyStep.tsx @@ -162,7 +162,7 @@ export function CompanyStep({
{error ? ( -
+
{error}
) : null} diff --git a/src/renderer/components/CompanyOnboarding/ConnectorReadiness.tsx b/src/renderer/components/CompanyOnboarding/ConnectorReadiness.tsx index 95fff1ae..b3b84565 100644 --- a/src/renderer/components/CompanyOnboarding/ConnectorReadiness.tsx +++ b/src/renderer/components/CompanyOnboarding/ConnectorReadiness.tsx @@ -39,11 +39,11 @@ export function ConnectorReadiness({ {isChecking ? ( ) : isReady ? ( - + ) : isMissing ? ( ) : ( - + )}
@@ -75,7 +75,7 @@ export function ConnectorReadiness({ })}
{!hasRunnableConnector ? ( -
+
{t("onboarding.connectorWarning")}
) : null} diff --git a/src/renderer/components/CompanyOnboarding/LaunchStep.tsx b/src/renderer/components/CompanyOnboarding/LaunchStep.tsx index 25c051e3..86c806a1 100644 --- a/src/renderer/components/CompanyOnboarding/LaunchStep.tsx +++ b/src/renderer/components/CompanyOnboarding/LaunchStep.tsx @@ -28,7 +28,7 @@ export function LaunchStep({ transition={{ duration: 0.36, ease: [0.16, 1, 0.3, 1] }} className="bg-card border border-border shadow-sm w-full rounded-lg p-7" > -
+

@@ -72,7 +72,7 @@ export function LaunchStep({

{error ? ( -
+
{error}
) : null} diff --git a/src/renderer/components/CompanyOnboarding/MissionStep.tsx b/src/renderer/components/CompanyOnboarding/MissionStep.tsx index 41af6da2..ba3a79ad 100644 --- a/src/renderer/components/CompanyOnboarding/MissionStep.tsx +++ b/src/renderer/components/CompanyOnboarding/MissionStep.tsx @@ -70,7 +70,7 @@ export function MissionStep({
{error ? ( -
+
{error}
) : null} diff --git a/src/renderer/components/CompanyOnboarding/NodeCheckStep.tsx b/src/renderer/components/CompanyOnboarding/NodeCheckStep.tsx index 15b1d4c4..f4b51ad3 100644 --- a/src/renderer/components/CompanyOnboarding/NodeCheckStep.tsx +++ b/src/renderer/components/CompanyOnboarding/NodeCheckStep.tsx @@ -80,7 +80,7 @@ export function NodeCheckStep({ {checking ? ( ) : nodeStatus?.installed ? ( - + ) : nodeStatus ? ( ) : null} @@ -88,7 +88,7 @@ export function NodeCheckStep({ {checking ? ( {t("onboarding.providerChecking")}... ) : nodeStatus?.installed ? ( - + {t("onboarding.nodeInstalled").replace("{version}", nodeStatus.version ?? "")} ) : nodeStatus ? ( diff --git a/src/renderer/components/CompanyOnboarding/OnboardingHero.tsx b/src/renderer/components/CompanyOnboarding/OnboardingHero.tsx index f60b8e9a..a05fc231 100644 --- a/src/renderer/components/CompanyOnboarding/OnboardingHero.tsx +++ b/src/renderer/components/CompanyOnboarding/OnboardingHero.tsx @@ -63,7 +63,7 @@ export function OnboardingHero({ initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.42, ease: [0.16, 1, 0.3, 1] }} - className="relative z-10 mx-auto w-full max-w-[460px]" + className="relative z-10 mx-auto w-full max-w-[min(460px,90vw)]" > {/* Signature line */} string }) { if (status === "ready") { return ( - - + + {t("onboarding.providerReady")} ); @@ -77,8 +77,8 @@ function StatusBadge({ status, t }: { status: ConnectorStatus; t: (key: string) } if (status === "auth_required") { return ( - - + + {t("onboarding.providerAuthRequired")} ); @@ -92,8 +92,8 @@ function StatusBadge({ status, t }: { status: ConnectorStatus; t: (key: string) ); } return ( - - + + {status.replaceAll("_", " ")} ); @@ -611,7 +611,7 @@ export function ProvidersStep({ {/* Error message */} {error ? ( -
+
{error}
) : null} diff --git a/src/renderer/components/Toast.tsx b/src/renderer/components/Toast.tsx index 7b07ac28..a308e216 100644 --- a/src/renderer/components/Toast.tsx +++ b/src/renderer/components/Toast.tsx @@ -6,7 +6,7 @@ import { useReducedMotion } from "./ui" const TONE_CONFIG = { success: { icon: CheckCircle, bg: "var(--muted-foreground)", border: "var(--primary)", text: "var(--primary)" }, danger: { icon: XCircle, bg: "var(--destructive)", border: "var(--destructive)", text: "var(--destructive)" }, - warn: { icon: Warning, bg: "var(--muted-foreground)", border: "var(--destructive)", text: "var(--destructive)" }, + warn: { icon: Warning, bg: "var(--muted-foreground)", border: "var(--warning)", text: "var(--warning)" }, } as const export default function ToastContainer() { diff --git a/src/renderer/components/automation/AutomationPanel.tsx b/src/renderer/components/automation/AutomationPanel.tsx index 200c5dfe..1dd33ff1 100644 --- a/src/renderer/components/automation/AutomationPanel.tsx +++ b/src/renderer/components/automation/AutomationPanel.tsx @@ -15,8 +15,8 @@ function DeleteConfirmDialog({ open, entityName, onConfirm, onCancel }: { open: if (!open) return null return ( - e.stopPropagation()}> -

Delete

+ e.stopPropagation()}> +

Delete

Are you sure you want to delete {entityName}? This action cannot be undone.

@@ -41,15 +41,15 @@ function RulesPanel({ rules, companyId, onRefresh }: { rules: AutomationRuleReco useEffect(() => { void window.agentCompany.getAutomationLog(companyId).then((r) => { if (r.ok) setActLog(r.data) }) }, [companyId, rules]) const filtered = useMemo(() => filterStatus === "all" ? rules : rules.filter((r) => r.status === filterStatus), [rules, filterStatus]) - const counts = useMemo(() => ({ total: rules.length, active: rules.filter((r) => r.status === "active").length, paused: rules.filter((r) => r.status === "paused").length, error: rules.filter((r) => r.status === "error").length }), [rules]) + const counts = useMemo(() => { let active = 0, paused = 0, error = 0; for (const r of rules) { if (r.status === "active") active++; else if (r.status === "paused") paused++; else if (r.status === "error") error++; } return { total: rules.length, active, paused, error } }, [rules]) const handleToggle = useCallback(async (rule: AutomationRuleRecord) => { setActionError(null); try { const r = await window.agentCompany.toggleAutomationRule({ id: rule.id, companyId, status: (rule.status === "active" ? "paused" : "active") as "active"|"paused" }); if (!r.ok) { setActionError(r.error.message); return }; onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed") } }, [companyId, onRefresh]) const handleDelete = useCallback(async () => { if (!delTarget) return; setActionError(null); try { const r = await window.agentCompany.deleteAutomationRule({ id: delTarget.id, companyId }); if (!r.ok) { setActionError(r.error.message); setDelTarget(null); return }; setDelTarget(null); onRefresh() } catch (e) { setActionError(e instanceof Error ? e.message : "Failed"); setDelTarget(null) } }, [delTarget, companyId, onRefresh]) return (
- {actionError ?
{actionError}
: null} + {actionError ?
{actionError}
: null}

Automation Rules

{counts.active} active, {counts.paused} paused{counts.error > 0 ? `, ${counts.error} errored` : ""}

setShowCreate(true)} icon={Plus} tone="accent" />
- {rules.length > 0 ?
{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--primary)" }].map((s) =>
{s.l}
{s.v}
)}
: null} + {rules.length > 0 ?
{[{ l: "Total Rules", v: counts.total, c: undefined }, { l: "Total Executions", v: rules.reduce((s, r) => s + r.executionCount, 0), c: ACCENT }, { l: "Recent Activity", v: actLog.length, c: "var(--primary)" }].map((s) =>
{s.l}
{s.v}
)}
: null}
{activeTab === "rules" ? <> {rules.length > 0 ?
{(["all","active","paused","error"] as const).map((s) => )}
: null} @@ -71,7 +71,7 @@ function WorkflowsPanel({ workflows, companyId, onRefresh }: { workflows: Workfl const counts = useMemo(() => ({ total: workflows.length, active: workflows.filter((w) => w.status === "active").length, draft: workflows.filter((w) => w.status === "draft").length }), [workflows]) return (
- {actionError ?
{actionError}
: null} + {actionError ?
{actionError}
: null}

Workflow Pipelines

{counts.active} active, {counts.draft} draft{counts.total > counts.active + counts.draft ? `, ${counts.total - counts.active - counts.draft} other` : ""}

setShowCreate(true)} icon={Plus} tone="accent" />
{workflows.length === 0 ?
No workflow pipelines
Workflows chain actions into multi-step pipelines.
setShowCreate(true)} icon={ArrowsClockwise} tone="accent" />
:
{workflows.map((wf) => setDelTarget(wf)} onEdit={() => setEditWf(wf)} onRun={() => void handleRun(wf)} />)}
} diff --git a/src/renderer/components/automation/RuleCard.tsx b/src/renderer/components/automation/RuleCard.tsx index c090651d..d10c61dd 100644 --- a/src/renderer/components/automation/RuleCard.tsx +++ b/src/renderer/components/automation/RuleCard.tsx @@ -27,7 +27,7 @@ export function RuleCard({ rule, onToggle, onDelete, onEdit }: { rule: Automatio
- +
diff --git a/src/renderer/components/automation/RuleEditor.tsx b/src/renderer/components/automation/RuleEditor.tsx index fb0be974..0b36748d 100644 --- a/src/renderer/components/automation/RuleEditor.tsx +++ b/src/renderer/components/automation/RuleEditor.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react" import { motion } from "framer-motion" import { Gear, Lightning, X } from "@phosphor-icons/react" import type { AutomationAction, AutomationRuleRecord, AutomationTrigger, DepartmentId } from "@shared/types" -import { ActionButton, Select } from "../ui" +import { ActionButton, Select, useFocusTrap } from "../ui" import { ACCENT, LINE, triggerConfig, actionConfig, departmentOptions, safeJsonStringify } from "./automation-config" interface RuleFormState { name: string; description: string; trigger: AutomationTrigger; conditionsJson: string; action: AutomationAction; actionConfigJson: string; sourceDepartment: string; targetDepartment: string; priority: number } @@ -20,18 +20,20 @@ export function RuleFormModal({ open, editingRule, companyId, onClose, onSaved } setSaving(true); setError(null) try { const result = await window.agentCompany.saveAutomationRule({ id: editingRule?.id ?? null, companyId, name: form.name.trim(), description: form.description.trim(), trigger: form.trigger, conditionsJson: form.conditionsJson, action: form.action, actionConfigJson: form.actionConfigJson, sourceDepartment: (form.sourceDepartment || null) as DepartmentId | null, targetDepartment: (form.targetDepartment || null) as DepartmentId | null, priority: form.priority, status: editingRule?.status ?? "active" }); if (!result.ok) { setError(result.error.message); return }; onSaved(); onClose() } catch (err) { setError(err instanceof Error ? err.message : "Failed to save rule") } finally { setSaving(false) } }, [form, editingRule, companyId, onSaved, onClose]) + const trapRef = useFocusTrap(open) + useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey) }, [open, onClose]) if (!open) return null const triggerOptions = Object.entries(triggerConfig).map(([key, c]) => ({ value: key, label: c.label })); const actionOptions = Object.entries(actionConfig).map(([key, c]) => ({ value: key, label: c.label })) const selectedTriggerInfo = triggerConfig[form.trigger]; const selectedActionInfo = actionConfig[form.action] return ( - e.stopPropagation()}> + e.stopPropagation()}>

{isEditing ? "Edit Automation Rule" : "New Automation Rule"}

- {error ?
{error}
: null} + {error ?
{error}
: null}
updateField("name", e.target.value)} placeholder="e.g., Auto-assign blocked tasks" className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 w-full rounded-lg border bg-card px-3.5 py-2.5 text-[14px] text-foreground placeholder:text-muted-foreground" style={{ borderColor: LINE }} />