Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/apps/cli/src/peer_host/deny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,50 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[
// belongs to the person at this machine, so refuse it explicitly rather
// than relying on the command being unimplemented here.
"git_trust_repository",
// Controller app-shell state mirrored from the FE deny list. An older or
// non-Web-UI controller can still HostInvoke these onto this peer, so the
// CLI peer host must refuse them independently of the FE optimization.
// Keep in sync with `src/web-ui/.../adapters/peer-device-adapter.ts`
// LOCAL_ONLY_COMMANDS and `src/apps/desktop/src/api/peer_host_invoke.rs`.
// These controller-owned commands are not implemented here either, but
// being unimplemented is not the boundary — refuse explicitly.
"i18n_get_current_language",
"i18n_set_language",
"i18n_get_supported_languages",
"i18n_get_config",
"i18n_set_config",
"get_pending_announcements",
"get_announcement_tips",
"mark_announcement_seen",
"dismiss_announcement",
"never_show_announcement",
"trigger_announcement",
"list_agent_companion_pets",
"import_agent_companion_pet_package",
"delete_agent_companion_pet_package",
"generate_insights",
"get_latest_insights",
"load_insights_report",
"has_insights_data",
"cancel_insights_generation",
"report_ide_control_result",
"browser_control_launch",
"browser_control_list_browsers",
"browser_control_get_status",
"browser_control_restart_with_cdp",
"browser_control_enable_default_cdp",
"browser_webview_create",
"browser_webview_eval",
"browser_webview_navigate",
"browser_webview_reload",
"browser_webview_set_bounds",
"computer_use_get_status",
"debug_devtools_available",
"debug_open_devtools",
"resize_agent_companion_desktop_pet",
"show_agent_companion_desktop_pet",
"hide_agent_companion_desktop_pet",
"append_flow_chat_diagnostics",
];

/// Desktop IDE surfaces that CLI Peer Host does not implement.
Expand Down
55 changes: 55 additions & 0 deletions src/apps/desktop/src/api/peer_host_invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,61 @@ static LOCAL_ONLY_COMMANDS: &[&str] = &[
// That decision stays with the person at that machine; a controller can
// still read `git_get_repository_trust` and relay the manual command.
"git_trust_repository",
// Controller app-shell state mirrored from the FE deny list. An older or
// non-Web-UI controller can still HostInvoke these onto this peer, so the
// peer host must refuse them independently of the FE optimization. Keep in
// sync with `src/web-ui/.../adapters/peer-device-adapter.ts`
// LOCAL_ONLY_COMMANDS and `src/apps/cli/src/peer_host/deny.rs`.
// UI locale writes the controller's config and rebuilds THIS machine's
// macOS menubar/tray; routing it to a peer writes the wrong config.
"i18n_get_current_language",
"i18n_set_language",
"i18n_get_supported_languages",
"i18n_get_config",
"i18n_set_config",
// Announcement scheduler/state: get_pending / get_tips run the scheduler
// (mutate app_open_count + persist); seen / dismiss / never-show write
// controller announcement state. Refused on the peer.
"get_pending_announcements",
"get_announcement_tips",
"mark_announcement_seen",
"dismiss_announcement",
"never_show_announcement",
"trigger_announcement",
// Companion pets live on the controller's desktop; the import zip path is
// picked by a local dialog on the controller and is not readable here.
"list_agent_companion_pets",
"import_agent_companion_pet_package",
"delete_agent_companion_pet_package",
// Insights is the controller's own usage report: it reads the controller's
// session history and writes the HTML to the controller's user_data_dir.
"generate_insights",
"get_latest_insights",
"load_insights_report",
"has_insights_data",
"cancel_insights_generation",
// IDE control events drive the controller window's panels; the result
// report must settle on the controller's transport, not here.
"report_ide_control_result",
// Controller app-shell / local-device commands (browser/webview/DevTools/
// desktop-pet/diagnostics) operate on the controller's own surfaces.
"browser_control_launch",
"browser_control_list_browsers",
"browser_control_get_status",
"browser_control_restart_with_cdp",
"browser_control_enable_default_cdp",
"browser_webview_create",
"browser_webview_eval",
"browser_webview_navigate",
"browser_webview_reload",
"browser_webview_set_bounds",
"computer_use_get_status",
"debug_devtools_available",
"debug_open_devtools",
"resize_agent_companion_desktop_pet",
"show_agent_companion_desktop_pet",
"hide_agent_companion_desktop_pet",
"append_flow_chat_diagnostics",
];

static PENDING: OnceLock<Mutex<HashMap<String, oneshot::Sender<HostInvokeBridgeResult>>>> =
Expand Down
55 changes: 54 additions & 1 deletion src/web-ui/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,63 @@ export default tseslint.config(
'src/**/*.example.tsx',
'src/component-library/components/registry.tsx',
'src/component-library/preview/**',
// Pre-existing legacy: context-system type impls use class components
// with hooks and other legacy patterns. Kept out of lint to avoid
// unrelated churn; the dynamic-import fence still covers the rest of
// src/**. FileContextImpl.tsx here is migrated to api.invoke.
'src/shared/context-system/core/types/**',
'src/shared/context-menu-system/examples/**',
],
},
{
// Adapter-layer fence: business Tauri commands must reach the platform
// only through ApiClient (api.invoke). Direct `invoke` from
// '@tauri-apps/api/core' is reserved for the adapter implementations in
// adapters/** (and the peer-device host bridge, which intentionally runs
// outside the routed transport — see PeerHostInvokeBridge). This is the
// executable form of "front end calls go through the adapter layer";
// reintroducing a direct invoke elsewhere fails the build.
files: ['src/**/*.{ts,tsx}'],
ignores: [
'src/infrastructure/api/adapters/**',
// PeerHostInvokeBridge runs on the HOST side of Peer-Device Mode: it
// executes *dynamic* command names forwarded from the peer device via a
// raw Tauri invoke. ApiClient is already routed to the peer adapter at
// this point, so routing through it would be wrong. This is the one
// intentional exception to the adapter-layer fence.
'src/infrastructure/peer-device/PeerHostInvokeBridge.tsx',
],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['@tauri-apps/api/core'],
importNames: ['invoke'],
message:
'业务命令必须经 api.invoke(ApiClient) 统一适配层,不可直接 import invoke。' +
'如需直连平台 invoke,放到 adapters/ 内并经 api 暴露。',
},
],
},
],
// no-restricted-imports only covers static ImportDeclaration in ESLint 9;
// dynamic `import('@tauri-apps/api/core')` to grab `invoke` bypasses it.
// Block the same surface with an ImportExpression selector so a future
// dynamic-import bypass fails the build too. Same ignores (adapters/** +
// PeerHostInvokeBridge) apply via this block's ignores; exceptions must be
// added with an owner comment, like the static rule.
'no-restricted-syntax': [
'error',
{
selector: "ImportExpression[source.value='@tauri-apps/api/core']",
message:
'业务命令必须经 api.invoke(ApiClient) 统一适配层,不可动态 import invoke。' +
'如需直连平台 invoke,放到 adapters/ 内并经 api 暴露。',
},
],
},
},
{
files: ['src/**/*.{ts,tsx}'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
Expand Down
7 changes: 3 additions & 4 deletions src/web-ui/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SessionUsageModal } from '../flow_chat/components/usage/SessionUsageMod
import { createLogger } from '@/shared/utils/logger';
import { startupTrace } from '@/shared/utils/startupTrace';
import { isTauriRuntime } from '@/infrastructure/runtime';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import { useWorkspaceContext } from '../infrastructure/contexts/WorkspaceContext';
import { useGlobalSceneShortcuts } from './hooks/useGlobalSceneShortcuts';
import { useDebugInspector } from '@/infrastructure/debug/useDebugInspector';
Expand Down Expand Up @@ -225,8 +226,7 @@ function App() {
mainWindowShownRef.current = true;

try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('show_main_window');
await api.invoke('show_main_window');
log.debug('Main window shown', { reason });
startupTrace.markPhase('main_window_shown', { reason });
window.dispatchEvent(new CustomEvent('bitfun:main-window-shown', { detail: { reason } }));
Expand Down Expand Up @@ -663,8 +663,7 @@ function App() {
await openAgentCompanionSession(sessionId);

try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('show_main_window');
await api.invoke('show_main_window');
} catch (error) {
log.warn('Failed to show main window from Agent companion bubble', {
sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { emit, listen } from '@tauri-apps/api/event';
import { cursorPosition, getCurrentWindow } from '@tauri-apps/api/window';
import { aiExperienceConfigService, type AgentCompanionPetSelection, type AIExperienceSettings } from '@/infrastructure/config/services/AIExperienceConfigService';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import { ChatInputPixelPet, type ChatInputPixelPetMood } from '@/flow_chat/components/ChatInputPixelPet';
import type { ChatInputPetMood } from '@/flow_chat/utils/chatInputPetMood';
import type {
Expand Down Expand Up @@ -412,11 +413,10 @@ export const AgentCompanionDesktopPet: React.FC = () => {
return;
}

void import('@tauri-apps/api/core')
.then(({ invoke }) => invoke('resize_agent_companion_desktop_pet', {
void api.invoke('resize_agent_companion_desktop_pet', {
width: nextWidth,
height: nextHeight,
}))
})
.catch(error => {
log.warn('Failed to resize Agent companion window', error);
});
Expand Down Expand Up @@ -568,8 +568,7 @@ export const AgentCompanionDesktopPet: React.FC = () => {

const showMainWindowFromPet = useCallback(async () => {
try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('show_main_window');
await api.invoke('show_main_window');
} catch (error) {
log.warn('Failed to show main window from Agent companion pet', error);
}
Expand Down Expand Up @@ -824,12 +823,8 @@ export const AgentCompanionDesktopPet: React.FC = () => {

const openTaskSession = async (task: AgentCompanionTaskStatus) => {
try {
const [{ invoke }, { emit }] = await Promise.all([
import('@tauri-apps/api/core'),
import('@tauri-apps/api/event'),
]);
await emit('agent-companion://open-session', { sessionId: task.sessionId });
await invoke('show_main_window');
await api.invoke('show_main_window');
} catch (error) {
log.warn('Failed to open Agent companion task session', {
sessionId: task.sessionId,
Expand Down
8 changes: 3 additions & 5 deletions src/web-ui/src/app/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { useSessionModeStore } from '../stores/sessionModeStore';
import { isMacOSDesktopRuntime } from '@/infrastructure/runtime';
import { flowChatSessionConfigForWorkspace } from '../utils/projectSessionWorkspace';
import { notificationService } from '@/shared/notification-system';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import { AppearanceBackgroundMediaLayer, appearanceRuntime, useAppearance } from '@/infrastructure/appearance';
import './AppLayout.scss';

Expand Down Expand Up @@ -445,10 +446,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ className = '' }) => {
try {
// Both macOS and Windows/Linux: Rust intercepts the native close request
// and emits this event. We decide hide vs quit; persist interrupted turns only on quit.
const [{ listen }, { invoke }] = await Promise.all([
import('@tauri-apps/api/event'),
import('@tauri-apps/api/core'),
]);
const { listen } = await import('@tauri-apps/api/event');

const persistInterruptedTurnsForExit = async () => {
try {
Expand All @@ -466,7 +464,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ className = '' }) => {
if (isMacOS) {
// macOS always hides to keep the app alive in the dock.
try {
await invoke('hide_main_window_after_close_request');
await api.invoke('hide_main_window_after_close_request');
} catch (error) {
log.error('Failed to hide main window after close request', error);
}
Expand Down
4 changes: 2 additions & 2 deletions src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { TFunction } from 'i18next';
import { agentAPI, type ModeInfo } from '@/infrastructure/api/service-api/AgentAPI';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import type { AgentSource } from '@/infrastructure/api/service-api/CustomAgentAPI';
import { SubagentAPI, type SubagentInfo } from '@/infrastructure/api/service-api/SubagentAPI';
import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI';
Expand Down Expand Up @@ -183,8 +184,7 @@ export function useAgentsList({

const fetchTools = async (): Promise<ToolInfo[]> => {
try {
const { invoke } = await import('@tauri-apps/api/core');
return await invoke<ToolInfo[]>('get_all_tools_info');
return await api.invoke<ToolInfo[]>('get_all_tools_info');
} catch {
return [];
}
Expand Down
20 changes: 7 additions & 13 deletions src/web-ui/src/app/scenes/browser/useEmbeddedBrowserWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BLANK_TARGET_INTERCEPT_SCRIPT } from './browserInspectorScript';
import { STREAM_RENDER_OPTIMIZATION_SCRIPT } from './browserStreamPerformanceScript';
import { validateUrl } from './browserUrlCheck';
import { api } from '@/infrastructure/api/service-api/ApiClient';

const WEBVIEW_RESIZE_DEBOUNCE_MS = 160;
const WEBVIEW_BOUNDS_EPSILON = 1;
Expand Down Expand Up @@ -109,27 +110,23 @@ function normalizeUrl(raw: string, defaultUrl: string): string {
}

async function evalWebview(label: string, script: string): Promise<void> {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('browser_webview_eval', { request: { label, script } });
await api.invoke('browser_webview_eval', { request: { label, script } });
}

async function injectBrowserPageScripts(label: string): Promise<void> {
await evalWebview(label, `${BLANK_TARGET_INTERCEPT_SCRIPT};\n${STREAM_RENDER_OPTIMIZATION_SCRIPT};`);
}

async function navigateWebview(label: string, url: string): Promise<void> {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('browser_webview_navigate', { request: { label, url } });
await api.invoke('browser_webview_navigate', { request: { label, url } });
}

async function reloadWebview(label: string): Promise<void> {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('browser_webview_reload', { request: { label } });
await api.invoke('browser_webview_reload', { request: { label } });
}

async function setWebviewBounds(label: string, bounds: WebviewBounds): Promise<void> {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('browser_webview_set_bounds', {
await api.invoke('browser_webview_set_bounds', {
request: {
label,
x: bounds.left,
Expand All @@ -141,11 +138,8 @@ async function setWebviewBounds(label: string, bounds: WebviewBounds): Promise<v
}

async function createBrowserWebview(label: string, url: string, bounds: WebviewBounds): Promise<BrowserWebviewHandle> {
const [{ invoke }, { Webview }] = await Promise.all([
import('@tauri-apps/api/core'),
import('@tauri-apps/api/webview'),
]);
await invoke('browser_webview_create', {
const { Webview } = await import('@tauri-apps/api/webview');
await api.invoke('browser_webview_create', {
request: {
label,
url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GalleryZone } from '@/app/components';
import '@/app/components/GalleryLayout/GalleryLayout.scss';
import { Switch } from '@/component-library';
import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI';
import { api } from '@/infrastructure/api/service-api/ApiClient';
import type { AgentProfileConfigItem, ModeSkillInfo } from '@/infrastructure/config/types';
import {
buildSkillCoverageSourceMap,
Expand Down Expand Up @@ -183,10 +184,9 @@ const AssistantDefaultsPage: React.FC = () => {
(async () => {
setLoading(true);
try {
const { invoke } = await import('@tauri-apps/api/core');
const [modeConf, tools, skillList, servers] = await Promise.all([
configAPI.getAgentProfileConfig(ASSISTANT_MODE_ID).catch(() => null as AgentProfileConfigItem | null),
invoke<ToolInfo[]>('get_all_tools_info').catch(() => [] as ToolInfo[]),
api.invoke<ToolInfo[]>('get_all_tools_info').catch(() => [] as ToolInfo[]),
configAPI.getModeSkillConfigs({ modeId: ASSISTANT_MODE_ID }).catch(() => [] as ModeSkillInfo[]),
MCPAPI.getServers().catch(() => [] as MCPServerInfo[]),
]);
Expand Down
6 changes: 4 additions & 2 deletions src/web-ui/src/app/services/agentCompanionPetCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ const saveSettingsMock = vi.hoisted(() => vi.fn(() => Promise.resolve()));
const openSettingsMock = vi.hoisted(() => vi.fn());
const invokeMock = vi.hoisted(() => vi.fn(() => Promise.resolve()));

vi.mock('@tauri-apps/api/core', () => ({
invoke: invokeMock,
vi.mock('@/infrastructure/api/service-api/ApiClient', () => ({
api: {
invoke: invokeMock,
},
}));

vi.mock('@/flow_chat/services/FlowChatManager', () => ({
Expand Down
Loading