diff --git a/src/crates/core/src/miniapp/builtin/assets/coding-selfie/style.css b/src/crates/core/src/miniapp/builtin/assets/coding-selfie/style.css index 086ac38b2a..c8434a533e 100644 --- a/src/crates/core/src/miniapp/builtin/assets/coding-selfie/style.css +++ b/src/crates/core/src/miniapp/builtin/assets/coding-selfie/style.css @@ -757,6 +757,39 @@ html, body { } .cs-empty.is-error .cs-empty-art { color: var(--cs-del); } +.cs-empty-action { + margin-top: 12px; + padding: 8px 16px; + border-radius: var(--cs-radius-sm); + background: var(--cs-accent); + color: var(--cs-bg); + font-family: var(--cs-sans); + font-size: 13px; + font-weight: 500; + border: none; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: background 140ms ease, transform 100ms ease; + flex-shrink: 0; +} +.cs-empty-action:hover { + background: var(--cs-accent-2); +} +.cs-empty-action:active { + transform: scale(0.97); +} +.cs-empty-action svg { + width: 14px; + height: 14px; +} + +.cs-empty.has-action { + flex-direction: column; + align-items: flex-start; +} + /* ─── Adaptive: gracefully shed less-essential tiles ── The dashboard prefers to fit on a single screen on ANY aspect ratio. When space gets tight we hide optional blocks instead of overflowing. */ diff --git a/src/crates/core/src/miniapp/builtin/assets/coding-selfie/ui.js b/src/crates/core/src/miniapp/builtin/assets/coding-selfie/ui.js index 6acc5b60cb..172125fde0 100644 --- a/src/crates/core/src/miniapp/builtin/assets/coding-selfie/ui.js +++ b/src/crates/core/src/miniapp/builtin/assets/coding-selfie/ui.js @@ -128,6 +128,8 @@ const I18N = { errScan: '扫描失败', errScanReason: (r) => `原因:${r}`, errScanRuntime: '扫描出错', + errGitNotFound: '未检测到 Git', + errGitNotFoundDesc: '请先安装 Git 并确保已添加到系统环境变量中。', styleNight: '凌晨刺客', styleMorning: '早起鸟', styleAm: '上午型选手', @@ -222,6 +224,8 @@ const I18N = { errScan: '掃描失敗', errScanReason: (r) => `原因:${r}`, errScanRuntime: '掃描出錯', + errGitNotFound: '未檢測到 Git', + errGitNotFoundDesc: '請先安裝 Git 並確保已添加到系統環境變量中。', styleNight: '凌晨刺客', styleMorning: '早起鳥', styleAm: '上午型選手', @@ -317,6 +321,8 @@ const I18N = { errScan: 'Scan failed', errScanReason: (r) => `reason: ${r}`, errScanRuntime: 'Scan error', + errGitNotFound: 'Git not found', + errGitNotFoundDesc: 'Please install Git and ensure it is added to your system PATH.', styleNight: 'Midnight Hacker', styleMorning: 'Early Bird', styleAm: 'Morning Brew', @@ -389,23 +395,51 @@ function greetingFor(d, name) { return t('greetWith')(part, name); } -function setEmpty(state, title, desc, icon) { +function setEmpty(state, title, desc, icon, actionLabel, actionUrl) { const el = $('empty'); - el.classList.remove('is-loading', 'is-error'); - if (state === 'loading') el.classList.add('is-loading'); - if (state === 'error') el.classList.add('is-error'); - $('empty-icon').innerHTML = icon || SVG.loader; - $('empty-title').textContent = title; - $('empty-desc').textContent = desc || ''; + el.classList.remove('is-loading', 'is-error', 'has-action'); + if (state === 'loading') el.classList.add('is-loading'); + if (state === 'error') { + el.classList.add('is-error'); + if (actionLabel && actionUrl) { + el.classList.add('has-action'); + } + } + $('empty-icon').innerHTML = icon || SVG.loader; + $('empty-title').textContent = title; + $('empty-desc').textContent = desc || ''; + + const actionEl = $('empty-action'); + if (actionLabel && actionUrl) { + if (!actionEl) { + actionEl = document.createElement('button'); + actionEl.className = 'cs-empty-action'; + actionEl.id = 'empty-action'; + const container = $('empty'); + container.appendChild(actionEl); + } + actionEl.textContent = actionLabel; + actionEl.onclick = () => { + if (window.app && window.app.system && window.app.system.openExternal) { + window.app.system.openExternal(actionUrl); + } else { + window.open(actionUrl, '_blank'); + } + }; + } else if (actionEl) { + actionEl.remove(); + } } -function showEmpty(state, title, desc, icon) { - setEmpty(state, title, desc, icon); - $('empty').removeAttribute('hidden'); - $('content').setAttribute('hidden', ''); + +function showEmpty(state, title, desc, icon, actionLabel, actionUrl) { + setEmpty(state, title, desc, icon, actionLabel, actionUrl); + $('empty').removeAttribute('hidden'); + $('content').setAttribute('hidden', ''); } + function hideEmpty() { - $('empty').setAttribute('hidden', ''); - $('content').removeAttribute('hidden'); + $('empty').setAttribute('hidden', ''); + $('content').removeAttribute('hidden'); } function renderDonut(langs) { @@ -1280,6 +1314,15 @@ async function scanGitWorkspace(cwd) { let lastData = null; let scanning = false; +async function checkGitAvailable() { + try { + await window.app.shell.exec(['git', '--version'], { timeout: 5000 }); + return true; + } catch (_e) { + return false; + } +} + async function scan() { if (scanning) return; scanning = true; @@ -1293,6 +1336,14 @@ async function scan() { return; } + const gitAvailable = await checkGitAvailable(); + if (!gitAvailable) { + showEmpty('error', t('errGitNotFound'), t('errGitNotFoundDesc'), SVG.gitBranch, t('errGitNotFoundAction'), t('gitDownloadUrl')); + lastData = null; + scanning = false; + return; + } + if (!lastData) { showEmpty('loading', t('loadingTitle'), shortPath(ws), SVG.loader); } diff --git a/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts b/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts index c1e124788b..4ce08909fe 100644 --- a/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts +++ b/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts @@ -63,10 +63,6 @@ export const SETTINGS_TAB_SEARCH_CONTENT: Record = ({ variant } const [confirmationTimeout, setConfirmationTimeout] = useState(''); const [toolExecConfigLoading, setToolExecConfigLoading] = useState(false); - const [computerUseEnabled, setComputerUseEnabled] = useState(false); - const [computerUseAccess, setComputerUseAccess] = useState(false); - const [computerUseScreen, setComputerUseScreen] = useState(false); - const [computerUseBusy, setComputerUseBusy] = useState(false); - const [computerUseStatusLoading, setComputerUseStatusLoading] = useState(false); - // ── Browser control state ─────────────────────────────────────────────── const [browserCdpAvailable, setBrowserCdpAvailable] = useState(false); const [browserKind, setBrowserKind] = useState(''); @@ -123,24 +110,6 @@ const SessionSettingsPanels: React.FC = ({ variant } const [expandedTemplates, setExpandedTemplates] = useState>(new Set()); const [isTemplatesModalOpen, setIsTemplatesModalOpen] = useState(false); - const refreshComputerUseStatus = useCallback(async (): Promise => { - if (!IS_TAURI_DESKTOP) return false; - setComputerUseStatusLoading(true); - try { - const { invoke } = await import('@tauri-apps/api/core'); - const s = await invoke('computer_use_get_status'); - setComputerUseEnabled(s.computerUseEnabled); - setComputerUseAccess(s.accessibilityGranted); - setComputerUseScreen(s.screenCaptureGranted); - return true; - } catch (error) { - log.error('computer_use_get_status failed', error); - return false; - } finally { - setComputerUseStatusLoading(false); - } - }, []); - const refreshBrowserControlStatus = useCallback(async () => { if (!IS_TAURI_DESKTOP) return; setBrowserStatusLoading(true); @@ -168,22 +137,15 @@ const SessionSettingsPanels: React.FC = ({ variant } } }, []); - const refreshDesktopStatus = useCallback((computerUseCfg: boolean | null | undefined) => { - if (!IS_TAURI_DESKTOP) { - setComputerUseEnabled(computerUseCfg ?? false); - return; - } - - void refreshComputerUseStatus().then((ok) => { - if (!ok) setComputerUseEnabled(computerUseCfg ?? false); - }); + const refreshDesktopStatus = useCallback(() => { + if (!IS_TAURI_DESKTOP) return; void refreshBrowserControlStatus(); void systemAPI.getSystemInfo() .then((info) => setPlatform(info.platform || '')) .catch((error) => log.warn('getSystemInfo failed', error)); - }, [refreshComputerUseStatus, refreshBrowserControlStatus]); + }, [refreshBrowserControlStatus]); const loadAllData = useCallback(async () => { setIsLoading(true); @@ -196,7 +158,6 @@ const SessionSettingsPanels: React.FC = ({ variant } execTimeout, confirmTimeout, debugConfigData, - computerUseCfg, browserControlPreferredBrowser, loadedCompanionPets, ] = await Promise.all([ @@ -207,7 +168,6 @@ const SessionSettingsPanels: React.FC = ({ variant } configManager.getConfig('ai.tool_execution_timeout_secs'), configManager.getConfig('ai.tool_confirmation_timeout_secs'), configManager.getConfig('ai.debug_mode_config'), - configManager.getConfig('ai.computer_use_enabled'), configManager.getConfig('ai.browser_control_preferred_browser'), listAgentCompanionPets(), ]); @@ -222,7 +182,7 @@ const SessionSettingsPanels: React.FC = ({ variant } if (debugConfigData) setDebugConfig(debugConfigData); setPreferredBrowser(browserControlPreferredBrowser || DEFAULT_BROWSER_CONTROL_BROWSER); - refreshDesktopStatus(computerUseCfg); + refreshDesktopStatus(); } catch (error) { log.error('Failed to load session config data', error); setSettings(await aiExperienceConfigService.getSettingsAsync()); @@ -414,37 +374,6 @@ const SessionSettingsPanels: React.FC = ({ variant } } }; - const handleComputerUseEnabledChange = async (checked: boolean) => { - setComputerUseBusy(true); - setComputerUseEnabled(checked); - try { - await configManager.setConfig('ai.computer_use_enabled', checked); - const { globalEventBus } = await import('@/infrastructure/event-bus'); - globalEventBus.emit('mode:config:updated'); - notificationService.success( - checked ? t('messages.saveSuccess') : t('messages.saveSuccess'), - { duration: 2000 } - ); - await refreshComputerUseStatus(); - } catch (error) { - log.error('Failed to save computer_use_enabled', error); - notificationService.error(t('messages.saveFailed')); - setComputerUseEnabled(!checked); - } finally { - setComputerUseBusy(false); - } - }; - - const handleComputerUseOpenSettings = async (pane: 'accessibility' | 'screen_capture') => { - try { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('computer_use_open_system_settings', { request: { pane } }); - } catch (error) { - log.error('computer_use_open_system_settings failed', error); - notificationService.error(t('messages.saveFailed')); - } - }; - const handleBrowserControlBrowserChange = async (value: string | number) => { const nextValue = String(value || DEFAULT_BROWSER_CONTROL_BROWSER); const previousValue = preferredBrowser; @@ -680,12 +609,6 @@ const SessionSettingsPanels: React.FC = ({ variant } const enabledModels = models.filter((m: AIModelConfig) => m.enabled); const sessionTitleModelId = funcAgentModels[AGENT_SESSION_TITLE] || 'fast'; const templateEntries = getTemplateEntries(); - const computerUseAccessLabel = computerUseStatusLoading - ? t('loading.text') - : computerUseAccess ? t('computerUse.granted') : t('computerUse.notGranted'); - const computerUseScreenLabel = computerUseStatusLoading - ? t('loading.text') - : computerUseScreen ? t('computerUse.granted') : t('computerUse.notGranted'); const browserStatusLabel = browserCdpAvailable ? `${browserKind} · ${browserPageCount} ${t('browserControl.tabs')}` : browserStatusLoading ? t('loading.text') : t('browserControl.notConnected'); @@ -1008,117 +931,6 @@ const SessionSettingsPanels: React.FC = ({ variant } - {/* ── Computer use (desktop) ─────────────────────────────── */} - - {IS_TAURI_DESKTOP ? ( - <> - -
- handleComputerUseEnabledChange(e.target.checked)} - disabled={computerUseBusy || computerUseStatusLoading} - size="small" - /> -
-
- -
- - - {computerUseAccessLabel} - - void refreshComputerUseStatus()} - > - - - - -
-
- -
- - - {computerUseScreenLabel} - - void refreshComputerUseStatus()} - > - - - - -
-
- - ) : null} -
- {/* ── Browser control (CDP) ──────────────────────────────── */}