mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
fix(mobile): keep Agent Mode off the emulated-mobile load path (#2587)
* fix(mobile): keep Agent Mode off the emulated-mobile load path `app.emulateMobile(true)` flips Platform.isMobile to true (stubbing Node's built-ins to null) but leaves Platform.isDesktopApp true — so gating Agent Mode on isDesktopApp still imported the @/agentMode barrel, whose module-scope `promisify(execFile)` then crashed the whole plugin with "Cannot read properties of null (reading 'promisify')". Add isDesktopRuntime() (Platform.isDesktopApp && !Platform.isMobile) and gate every Agent Mode load/registration on it so the barrel is never imported on a Node-less runtime. Update the mobile-load smoke gate rule to enforce the new gate and add a unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(lint): ban Platform.isDesktopApp; route desktop gating through isDesktopRuntime() Platform.isDesktopApp stays true under app.emulateMobile(true) (which stubs Node), so it doesn't keep desktop-only/Node code off the emulated-mobile path. Add a no-restricted-properties ESLint rule banning it (the canonical check in src/utils/desktopRuntime.ts is exempt via an inline disable), and migrate all 14 existing usages — web viewer, encryption/keychain, Miyo discovery, web-tab and @-mention chat hooks, and main.ts's web-viewer gates — to isDesktopRuntime(). Behavior is unchanged on real desktop and mobile; only emulateMobile now faithfully hides these desktop-only features. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e962c79f60
commit
ee870b4dce
18 changed files with 133 additions and 52 deletions
|
|
@ -119,6 +119,22 @@ export default [
|
|||
"Don't use the global `app` (footgun in popouts). Thread `app` via useApp() or a parameter. See designdocs/agents/PLUGIN_DEV_GUIDE.md.",
|
||||
},
|
||||
],
|
||||
|
||||
// Ban `Platform.isDesktopApp` — it stays true under
|
||||
// `app.emulateMobile(true)` (which stubs Node's built-ins to null), so
|
||||
// desktop-only / Node-dependent code still runs there and crashes the
|
||||
// plugin. Gate on isDesktopRuntime() (desktop app AND not mobile) instead.
|
||||
// The helper itself (src/utils/desktopRuntime.ts) is exempt via an inline
|
||||
// disable, since it owns the canonical check.
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
object: "Platform",
|
||||
property: "isDesktopApp",
|
||||
message:
|
||||
"Use isDesktopRuntime() from @/utils/desktopRuntime instead. Platform.isDesktopApp stays true under app.emulateMobile(true) (Node stubbed to null), so desktop-only/Node code still runs there and crashes.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -96,16 +96,16 @@ function checkAgentModeImportBoundaries() {
|
|||
}
|
||||
}
|
||||
|
||||
// Every dynamic Agent Mode import must be gated by `isDesktopRuntime()`,
|
||||
// NOT a bare `Platform.isDesktopApp`: the latter stays `true` under
|
||||
// `app.emulateMobile(true)` (which stubs Node to null), so it does not keep the
|
||||
// `@/agentMode` barrel off the emulated-mobile load path and the plugin crashes.
|
||||
const dynamicImports = Array.from(source.matchAll(dynamicAgentModeImport));
|
||||
if (dynamicImports.length > 0) {
|
||||
if (!source.includes("Platform.isDesktopApp")) {
|
||||
fail(`${relativePath}: dynamic Agent Mode import must be gated by Platform.isDesktopApp.`);
|
||||
}
|
||||
if (source.includes("Platform.isMobile")) {
|
||||
fail(
|
||||
`${relativePath}: Agent Mode gates should use Platform.isDesktopApp, not Platform.isMobile.`
|
||||
);
|
||||
}
|
||||
if (dynamicImports.length > 0 && !source.includes("isDesktopRuntime")) {
|
||||
fail(
|
||||
`${relativePath}: dynamic Agent Mode import must be gated by isDesktopRuntime() ` +
|
||||
`— Platform.isDesktopApp is true under app.emulateMobile(true).`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ import { shouldUseMiyo } from "@/miyo/miyoUtils";
|
|||
import { getAllQAMarkdownContent } from "@/search/searchUtils";
|
||||
import { NoteSelectedTextContext, WebSelectedTextContext } from "@/types/message";
|
||||
import { ensureFolderExists, isSourceModeOn } from "@/utils";
|
||||
import { Editor, MarkdownView, Notice, Platform, TFile } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { Editor, MarkdownView, Notice, TFile } from "obsidian";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { COMMAND_IDS, COMMAND_ICONS, COMMAND_NAMES, CommandId } from "@/constants";
|
||||
import { setSelectedTextContexts } from "@/aiParams";
|
||||
|
|
@ -127,8 +128,9 @@ export function registerCommands(plugin: CopilotPlugin) {
|
|||
});
|
||||
|
||||
// Agent Mode is always on, but requires subprocess support — register the
|
||||
// agent commands on desktop only.
|
||||
if (Platform.isDesktopApp) {
|
||||
// agent commands only where the Node runtime exists (real desktop, not
|
||||
// `emulateMobile`, where importing Agent Mode would crash).
|
||||
if (isDesktopRuntime()) {
|
||||
addCommand(plugin, COMMAND_IDS.OPEN_AGENT_CHAT_WINDOW, () => {
|
||||
void plugin.activateAgentView();
|
||||
});
|
||||
|
|
@ -576,8 +578,7 @@ export function registerCommands(plugin: CopilotPlugin) {
|
|||
|
||||
// Add web selection to chat context command (manual)
|
||||
addCommand(plugin, COMMAND_IDS.ADD_WEB_SELECTION_TO_CHAT_CONTEXT, async () => {
|
||||
const { Platform } = await import("obsidian");
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
new Notice("Web selection is only available on desktop");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AlertCircle, CheckCircle, CircleDashed, Loader2 } from "lucide-react";
|
||||
import { Platform, TFile, TFolder } from "obsidian";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ContextNoteBadge,
|
||||
|
|
@ -112,7 +113,7 @@ export const ChatContextMenu: React.FC<ChatContextMenuProps> = ({
|
|||
|
||||
const activeNoteVisible = includeActiveNote && !hasAnySelection && Boolean(currentActiveFile);
|
||||
const activeWebTabVisible =
|
||||
includeActiveWebTab && !hasAnySelection && Boolean(activeWebTab) && Platform.isDesktopApp;
|
||||
includeActiveWebTab && !hasAnySelection && Boolean(activeWebTab) && isDesktopRuntime();
|
||||
|
||||
const hasContext =
|
||||
uniqueNotes.length > 0 ||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Platform } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
|
||||
import { useApp } from "@/context";
|
||||
import { getWebViewerService } from "@/services/webViewerService/webViewerServiceSingleton";
|
||||
|
|
@ -21,7 +21,7 @@ const EMPTY_ACTIVE_WEB_TAB_STATE: ActiveWebTabStateSnapshot = {
|
|||
export function useActiveWebTabState(): ActiveWebTabStateSnapshot {
|
||||
const app = useApp();
|
||||
const [state, setState] = useState<ActiveWebTabStateSnapshot>(() => {
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
return EMPTY_ACTIVE_WEB_TAB_STATE;
|
||||
}
|
||||
try {
|
||||
|
|
@ -32,7 +32,7 @@ export function useActiveWebTabState(): ActiveWebTabStateSnapshot {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
setState(EMPTY_ACTIVE_WEB_TAB_STATE);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { Platform, TFile, TFolder } from "obsidian";
|
||||
import { TFile, TFolder } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { FileText, Wrench, Folder, Globe, Image } from "lucide-react";
|
||||
import { TypeaheadOption } from "@/components/chat-components/TypeaheadMenuContent";
|
||||
import type { WebTabContext } from "@/types/message";
|
||||
|
|
@ -93,7 +94,7 @@ export function useAtMentionCategories(showTools: boolean = false): CategoryOpti
|
|||
return showTools;
|
||||
}
|
||||
if (cat.category === "webTabs") {
|
||||
return Platform.isDesktopApp;
|
||||
return isDesktopRuntime();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { Platform, TFolder, TFile } from "obsidian";
|
||||
import { TFolder, TFile } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { FileText, Wrench, Folder, FileClock, Globe, CircleDashed } from "lucide-react";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { getToolDescription } from "@/tools/toolManager";
|
||||
|
|
@ -34,7 +35,7 @@ export function useAtMentionSearch(
|
|||
// - In category mode with a search query (searching across all categories)
|
||||
// - In search mode when webTabs category is selected
|
||||
const shouldEnableWebTabPolling =
|
||||
Platform.isDesktopApp &&
|
||||
isDesktopRuntime() &&
|
||||
((mode === "category" && query.trim().length > 0) ||
|
||||
(mode === "search" && selectedCategory === "webTabs"));
|
||||
const openWebTabs = useOpenWebTabs({ enabled: shouldEnableWebTabPolling });
|
||||
|
|
@ -91,7 +92,7 @@ export function useAtMentionSearch(
|
|||
|
||||
const webTabItems: AtMentionOption[] = useMemo(
|
||||
() =>
|
||||
Platform.isDesktopApp
|
||||
isDesktopRuntime()
|
||||
? openWebTabs.map((tab, index) => {
|
||||
const isLoaded = tab.isLoaded !== false;
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { App, Platform } from "obsidian";
|
||||
import { App } from "obsidian";
|
||||
import { useApp } from "@/context";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { getWebViewerService } from "@/services/webViewerService/webViewerServiceSingleton";
|
||||
import type { WebTabContext } from "@/types/message";
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ export function useOpenWebTabs(options: UseOpenWebTabsOptions = {}): WebTabConte
|
|||
}
|
||||
|
||||
// Web Viewer is desktop-only
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
setTabs([]);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
$getRoot,
|
||||
} from "lexical";
|
||||
import { Globe } from "lucide-react";
|
||||
import { Platform } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { ACTIVE_WEB_TAB_MARKER } from "@/constants";
|
||||
import { BasePillNode, getEditorDocument, SerializedBasePillNode } from "./BasePillNode";
|
||||
import { TruncatedPillText } from "./TruncatedPillText";
|
||||
|
|
@ -110,7 +110,7 @@ function ActiveWebTabPillComponent(): JSX.Element {
|
|||
const { activeWebTabForMentions } = useActiveWebTabState();
|
||||
|
||||
// Not supported on mobile
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
return (
|
||||
<PillBadge>
|
||||
<div className="tw-flex tw-items-center tw-gap-1">
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import { $getSelection, $isRangeSelection, TextNode } from "lexical";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Platform } from "obsidian";
|
||||
|
||||
import { useCustomCommands } from "@/commands/state";
|
||||
import { CustomCommandManager } from "@/commands/customCommandManager";
|
||||
import { sortSlashCommands } from "@/commands/customCommandUtils";
|
||||
import { logError } from "@/logger";
|
||||
import { useSettingsValue } from "@/settings/model";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { TypeaheadMenuPortal } from "@/components/chat-components/TypeaheadMenuPortal";
|
||||
import { TypeaheadOption } from "@/components/chat-components/TypeaheadMenuContent";
|
||||
import {
|
||||
|
|
@ -40,7 +40,7 @@ function useAgentSlashData(): AgentSlashData {
|
|||
const [data, setData] = useState<AgentSlashData>(EMPTY_AGENT_SLASH_DATA);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Platform.isDesktopApp) return;
|
||||
if (!isDesktopRuntime()) return;
|
||||
|
||||
let cancelled = false;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
// eslint-disable-next-line import/no-nodejs-modules
|
||||
import { Buffer } from "buffer";
|
||||
import { Platform } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
|
||||
interface SafeStorage {
|
||||
isEncryptionAvailable(): boolean;
|
||||
|
|
@ -37,7 +38,7 @@ let safeStorageInternal: SafeStorage | null = null;
|
|||
* @returns The Electron safeStorage instance, or `null` when unavailable.
|
||||
*/
|
||||
function getSafeStorage(): SafeStorage | null {
|
||||
if (!Platform.isDesktopApp && !Platform.isDesktop) return null;
|
||||
if (!isDesktopRuntime() && !Platform.isDesktop) return null;
|
||||
if (safeStorageInternal) return safeStorageInternal;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
|
|
|
|||
25
src/main.ts
25
src/main.ts
|
|
@ -67,6 +67,7 @@ import {
|
|||
} from "@/settings/model";
|
||||
import { dehydrateDeviceProfile, hydrateDeviceProfile } from "@/settings/deviceProfiles";
|
||||
import { getDeviceId } from "@/utils/deviceId";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { installRendererEventsShim } from "@/utils/rendererEventsShim";
|
||||
import { ProjectContextCache } from "@/cache/projectContextCache";
|
||||
import { ContextProcessor } from "@/contextProcessor";
|
||||
|
|
@ -88,7 +89,6 @@ import {
|
|||
MarkdownView,
|
||||
Menu,
|
||||
Notice,
|
||||
Platform,
|
||||
Plugin,
|
||||
TFile,
|
||||
ViewCreator,
|
||||
|
|
@ -240,8 +240,11 @@ export default class CopilotPlugin extends Plugin {
|
|||
// Initialize ProjectManager
|
||||
this.projectManager = ProjectManager.getInstance(this.app, this);
|
||||
|
||||
// Initialize Agent Mode coordinator (desktop only — ACP needs subprocess support).
|
||||
if (Platform.isDesktopApp) {
|
||||
// Initialize Agent Mode coordinator (desktop only — ACP needs subprocess
|
||||
// support). Gate on `isDesktopRuntime()`, not `Platform.isDesktopApp`:
|
||||
// under `app.emulateMobile(true)` the latter stays true while Node is stubbed,
|
||||
// so importing the `@/agentMode` barrel there would crash the plugin at load.
|
||||
if (isDesktopRuntime()) {
|
||||
const {
|
||||
CopilotAgentView,
|
||||
PlanPreviewView,
|
||||
|
|
@ -302,7 +305,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
// Single source of truth for Active Web Tab ({activeWebTab}) state
|
||||
// Preserves activeWebTab when switching to Chat view
|
||||
// Only run on desktop - Web Viewer is not available on mobile
|
||||
if (Platform.isDesktopApp) {
|
||||
if (isDesktopRuntime()) {
|
||||
const { activeLeafRef, layoutRef } = startActiveWebTabTracking(this.app, {
|
||||
preserveOnViewTypes: [CHAT_VIEWTYPE],
|
||||
});
|
||||
|
|
@ -320,7 +323,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
(leaf: WorkspaceLeaf) => new RelevantNotesView(leaf, this)
|
||||
);
|
||||
if (
|
||||
Platform.isDesktopApp &&
|
||||
isDesktopRuntime() &&
|
||||
this.CopilotAgentView &&
|
||||
this.PlanPreviewView &&
|
||||
this.planPreviewViewType
|
||||
|
|
@ -470,8 +473,10 @@ export default class CopilotPlugin extends Plugin {
|
|||
this.projectRegister?.cleanup();
|
||||
this.settingsUnsubscriber?.();
|
||||
|
||||
// Tear down skills vault watchers + debounce timers.
|
||||
if (Platform.isDesktopApp) {
|
||||
// Tear down skills vault watchers + debounce timers. Gate matches onload so
|
||||
// we never import the `@/agentMode` barrel on a Node-less runtime (mobile /
|
||||
// emulateMobile), which would crash during unload.
|
||||
if (isDesktopRuntime()) {
|
||||
const { SkillManager } = await import("@/agentMode");
|
||||
if (SkillManager.hasInstance()) {
|
||||
SkillManager.getInstance().dispose();
|
||||
|
|
@ -738,7 +743,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
*/
|
||||
initWebSelectionWatcher() {
|
||||
// Only run on desktop
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -951,7 +956,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private requireAgentView(): AgentSessionManager | null {
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
new Notice("Agent Chat is not available on mobile.");
|
||||
return null;
|
||||
}
|
||||
|
|
@ -963,7 +968,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private canUseAgentView(): boolean {
|
||||
return !!this.agentSessionManager && Platform.isDesktopApp;
|
||||
return !!this.agentSessionManager && isDesktopRuntime();
|
||||
}
|
||||
|
||||
private isCopilotAgentView(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { logInfo, logWarn } from "@/logger";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { err2String } from "@/utils";
|
||||
import { Platform } from "obsidian";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
|
||||
/**
|
||||
* Service discovery payload for Miyo local service.
|
||||
|
|
@ -55,7 +55,7 @@ export class MiyoServiceDiscovery {
|
|||
return this.cachedBaseUrl;
|
||||
}
|
||||
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
*/
|
||||
|
||||
import type { App } from "obsidian";
|
||||
import { Platform } from "obsidian";
|
||||
|
||||
import { logError, logWarn } from "@/logger";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import * as actions from "@/services/webViewerService/webViewerServiceActions";
|
||||
import {
|
||||
getCommandManager,
|
||||
|
|
@ -73,7 +73,7 @@ export class WebViewerService {
|
|||
|
||||
/** Check if the current platform supports Web Viewer (desktop only). */
|
||||
isSupportedPlatform(): boolean {
|
||||
return Platform.isDesktopApp === true;
|
||||
return isDesktopRuntime();
|
||||
}
|
||||
|
||||
/** Get detailed Web Viewer availability information. */
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { ByokPanel, ModelManagementProvider } from "@/modelManagement";
|
|||
import { resetSettings } from "@/settings/model";
|
||||
import { CommandSettings } from "@/settings/v2/components/CommandSettings";
|
||||
import { Bot, Cog, Command, Cpu, Database, Sparkle, Sparkles, Wrench } from "lucide-react";
|
||||
import { Platform } from "obsidian";
|
||||
import React from "react";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { AdvancedSettings } from "./components/AdvancedSettings";
|
||||
import { BasicSettings } from "./components/BasicSettings";
|
||||
import { CopilotPlusSettings } from "./components/CopilotPlusSettings";
|
||||
|
|
@ -33,7 +33,7 @@ const DesktopOnlySettingsPanel: React.FC = () => (
|
|||
);
|
||||
|
||||
const AgentSettingsPanel: React.FC = () => {
|
||||
if (!Platform.isDesktopApp) return <DesktopOnlySettingsPanel />;
|
||||
if (!isDesktopRuntime()) return <DesktopOnlySettingsPanel />;
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<LazyAgentSettings />
|
||||
|
|
@ -42,7 +42,7 @@ const AgentSettingsPanel: React.FC = () => {
|
|||
};
|
||||
|
||||
const SkillsSettingsPanel: React.FC = () => {
|
||||
if (!Platform.isDesktopApp) return <DesktopOnlySettingsPanel />;
|
||||
if (!isDesktopRuntime()) return <DesktopOnlySettingsPanel />;
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<LazySkillsSettings />
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ import {
|
|||
import { ArrowUpRight, Info, Plus, ShieldCheck, Trash2, Unlock } from "lucide-react";
|
||||
import { ConfirmModal } from "@/components/modals/ConfirmModal";
|
||||
import { MigrateConfirmModal } from "@/components/modals/MigrateConfirmModal";
|
||||
import { type App, Notice, Platform } from "obsidian";
|
||||
import { type App, Notice } from "obsidian";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { isDesktopRuntime } from "@/utils/desktopRuntime";
|
||||
import { getPromptFilePath, SystemPromptAddModal } from "@/system-prompts";
|
||||
import { useSystemPrompts } from "@/system-prompts/state";
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ export const AdvancedSettings: React.FC = () => {
|
|||
const [frameLogPath, setFrameLogPath] = useState(DESKTOP_UNAVAILABLE_FRAME_LOG_PATH);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Platform.isDesktopApp) return;
|
||||
if (!isDesktopRuntime()) return;
|
||||
|
||||
let cancelled = false;
|
||||
void import("@/agentMode").then(({ acpFrameSink }) => {
|
||||
|
|
@ -491,7 +492,7 @@ export const AdvancedSettings: React.FC = () => {
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
new Notice("Agent Mode frame logs are available on desktop only.");
|
||||
return;
|
||||
}
|
||||
|
|
@ -510,7 +511,7 @@ export const AdvancedSettings: React.FC = () => {
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if (!Platform.isDesktopApp) {
|
||||
if (!isDesktopRuntime()) {
|
||||
new Notice("Agent Mode frame logs are available on desktop only.");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
35
src/utils/desktopRuntime.test.ts
Normal file
35
src/utils/desktopRuntime.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
jest.mock("obsidian", () => ({ Platform: { isDesktopApp: false, isMobile: false } }));
|
||||
|
||||
import { isDesktopRuntime } from "./desktopRuntime";
|
||||
|
||||
const obsidian: { Platform: { isDesktopApp: boolean; isMobile: boolean } } =
|
||||
jest.requireMock("obsidian");
|
||||
|
||||
function setPlatform(isDesktopApp: boolean, isMobile: boolean): void {
|
||||
obsidian.Platform.isDesktopApp = isDesktopApp;
|
||||
obsidian.Platform.isMobile = isMobile;
|
||||
}
|
||||
|
||||
describe("isDesktopRuntime", () => {
|
||||
it("is true on the real desktop app", () => {
|
||||
setPlatform(true, false);
|
||||
expect(isDesktopRuntime()).toBe(true);
|
||||
});
|
||||
|
||||
it("is false under app.emulateMobile(true) — isDesktopApp stays true but isMobile flips", () => {
|
||||
// The bug this guards: gating only on Platform.isDesktopApp let desktop-only
|
||||
// code load under emulateMobile (where Node is stubbed), crashing the plugin.
|
||||
setPlatform(true, true);
|
||||
expect(isDesktopRuntime()).toBe(false);
|
||||
});
|
||||
|
||||
it("is false on real mobile", () => {
|
||||
setPlatform(false, true);
|
||||
expect(isDesktopRuntime()).toBe(false);
|
||||
});
|
||||
|
||||
it("is false on any non-desktop runtime", () => {
|
||||
setPlatform(false, false);
|
||||
expect(isDesktopRuntime()).toBe(false);
|
||||
});
|
||||
});
|
||||
18
src/utils/desktopRuntime.ts
Normal file
18
src/utils/desktopRuntime.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Platform } from "obsidian";
|
||||
|
||||
/**
|
||||
* True only in a real desktop (Electron) app — an environment with Node and
|
||||
* subprocess support. Use this to gate any desktop-only feature (Agent Mode,
|
||||
* Node built-ins, the web viewer, etc.).
|
||||
*
|
||||
* `Platform.isDesktopApp` alone is NOT sufficient: `app.emulateMobile(true)`
|
||||
* keeps `isDesktopApp === true` (you're still in the Electron binary) but stubs
|
||||
* Node's built-in modules to `null` to mimic mobile, so desktop-only code that
|
||||
* runs there crashes. The flag that flips under emulation *and* on real mobile
|
||||
* is `Platform.isMobile`, so the correct check is "desktop app AND not
|
||||
* (emulated-)mobile".
|
||||
*/
|
||||
export function isDesktopRuntime(): boolean {
|
||||
// eslint-disable-next-line no-restricted-properties -- this helper owns the canonical check
|
||||
return Platform.isDesktopApp && !Platform.isMobile;
|
||||
}
|
||||
Loading…
Reference in a new issue