feat(miyo): disable miyo on mobile without a remote server URL (#2328)

* feat(miyo): disable miyo on mobile without a remote server URL

Adds `shouldUseMiyo()` to `miyoUtils.ts` as the single source of truth
for whether Miyo should be active. On mobile, Miyo is silently ignored
unless an explicit remote server URL is configured, since local service
discovery is unavailable on mobile. The three previously duplicated
`shouldUseMiyo` checks in RetrieverFactory, findRelevantNotes, and
VectorStoreManager now delegate to this shared function.

Also drops the redundant `enableSemanticSearchV3` guard — the UI already
enforces that enabling Miyo enables semantic search and disabling semantic
search disables Miyo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(miyo): align RelevantNotes UI check with shouldUseMiyo

Replaces the local `shouldUseMiyoIndex` predicate (which duplicated the
self-host grace-period logic and missed the mobile platform guard) with a
direct call to `shouldUseMiyo` from miyoUtils. This ensures the UI's
index-state check matches what the backend actually uses, so the
"Build Index"/"No relevant notes" state is always correct on mobile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(miyo): update findRelevantNotes mock to use shouldUseMiyo

Replace the isSelfHostAccessValid mock with shouldUseMiyo, which is now
the single gate checked by findRelevantNotes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Wenzheng Jiang 2026-03-27 09:12:23 +09:00 committed by GitHub
parent d6283e3151
commit c0d79efb7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 40 additions and 48 deletions

View file

@ -10,7 +10,7 @@ import { useNoteDrag } from "@/hooks/useNoteDrag";
import { cn } from "@/lib/utils";
import { logWarn } from "@/logger";
import { SemanticSearchToggleModal } from "@/components/modals/SemanticSearchToggleModal";
import type { CopilotSettings } from "@/settings/model";
import { shouldUseMiyo } from "@/miyo/miyoUtils";
import {
findRelevantNotes,
getSimilarityCategory,
@ -30,30 +30,6 @@ import {
import { Notice, TFile } from "obsidian";
import React, { memo, useCallback, useEffect, useState } from "react";
const SELF_HOST_GRACE_PERIOD_MS = 15 * 24 * 60 * 60 * 1000;
/**
* Return true when Miyo-backed semantic index is expected to be active.
*
* @param settings - Current Copilot settings object.
* @returns True when Miyo mode and self-host validation are active.
*/
function shouldUseMiyoIndex(settings: CopilotSettings): boolean {
if (!settings.enableMiyo || !settings.enableSemanticSearchV3) {
return false;
}
if (settings.selfHostModeValidatedAt == null) {
return false;
}
if ((settings.selfHostValidationCount ?? 0) >= 3) {
return true;
}
return Date.now() - settings.selfHostModeValidatedAt < SELF_HOST_GRACE_PERIOD_MS;
}
function useRelevantNotes(refresher: number) {
const [relevantNotes, setRelevantNotes] = useState<RelevantNoteEntry[]>([]);
const [signalTick, setSignalTick] = useState(0);
@ -93,9 +69,9 @@ function useHasIndex(notePath: string, refresher: number) {
const VectorStoreManager = (await import("@/search/vectorStoreManager")).default;
const { getSettings } = await import("@/settings/model");
const settings = getSettings();
const shouldUseMiyo = shouldUseMiyoIndex(settings);
const useMiyo = shouldUseMiyo(settings);
if (shouldUseMiyo) {
if (useMiyo) {
const isEmpty = await VectorStoreManager.getInstance().isIndexEmpty();
setHasIndex(!isEmpty);
return;

View file

@ -1,5 +1,6 @@
import { isSelfHostAccessValid } from "@/plusUtils";
import { CopilotSettings, getSettings } from "@/settings/model";
import { App, FileSystemAdapter } from "obsidian";
import { App, FileSystemAdapter, Platform } from "obsidian";
/**
* Return the user-configured Miyo server URL, or "" to fall back to local service discovery.
@ -12,6 +13,28 @@ export function getMiyoCustomUrl(settings: CopilotSettings): string {
return (settings.miyoServerUrl || "").trim();
}
/**
* Single source of truth for whether Miyo should be used.
*
* Returns false when:
* - `enableMiyo` is off, or
* - self-host access is invalid, or
* - running on mobile without a remote server URL (local service discovery
* is unavailable on mobile, so Miyo can only work via an explicit URL).
*
* Note: `enableSemanticSearchV3` need not be checked the UI enforces that
* enabling Miyo also enables semantic search, and disabling semantic search
* also disables Miyo.
*
* @param settings - Current Copilot settings.
*/
export function shouldUseMiyo(settings: CopilotSettings): boolean {
if (!settings.enableMiyo || !isSelfHostAccessValid()) {
return false;
}
return !Platform.isMobile || !!getMiyoCustomUrl(settings);
}
/**
* Resolve the vault identifier sent to Miyo.
*

View file

@ -1,5 +1,6 @@
import { logInfo, logWarn } from "@/logger";
import { isSelfHostAccessValid, isSelfHostModeValid } from "@/plusUtils";
import { isSelfHostModeValid } from "@/plusUtils";
import { shouldUseMiyo } from "@/miyo/miyoUtils";
import { getSettings, CopilotSettings } from "@/settings/model";
import { App } from "obsidian";
import { SelfHostRetriever, VectorSearchBackend } from "./selfHostRetriever";
@ -323,7 +324,7 @@ export class RetrieverFactory {
* @returns True when Miyo should be used for semantic retrieval.
*/
private static shouldUseMiyo(settings: CopilotSettings): boolean {
return settings.enableMiyo && settings.enableSemanticSearchV3 && isSelfHostAccessValid();
return shouldUseMiyo(settings);
}
/**

View file

@ -2,9 +2,8 @@ import { TFile } from "obsidian";
import { getBacklinkedNotes, getLinkedNotes } from "@/noteUtils";
import { findRelevantNotes } from "@/search/findRelevantNotes";
import { MiyoClient } from "@/miyo/MiyoClient";
import { getMiyoVault } from "@/miyo/miyoUtils";
import { getMiyoVault, shouldUseMiyo } from "@/miyo/miyoUtils";
import { getSettings } from "@/settings/model";
import { isSelfHostAccessValid } from "@/plusUtils";
import VectorStoreManager from "@/search/vectorStoreManager";
jest.mock("@/noteUtils", () => ({
@ -50,10 +49,7 @@ jest.mock("@/miyo/MiyoClient", () => ({
jest.mock("@/miyo/miyoUtils", () => ({
getMiyoVault: jest.fn(),
getMiyoCustomUrl: jest.fn().mockReturnValue(""),
}));
jest.mock("@/plusUtils", () => ({
isSelfHostAccessValid: jest.fn(),
shouldUseMiyo: jest.fn(),
}));
jest.mock("@/logger", () => ({
@ -75,9 +71,7 @@ function createMarkdownFile(path: string): TFile {
describe("findRelevantNotes", () => {
const mockedGetSettings = getSettings as jest.MockedFunction<typeof getSettings>;
const mockedIsSelfHostAccessValid = isSelfHostAccessValid as jest.MockedFunction<
typeof isSelfHostAccessValid
>;
const mockedShouldUseMiyo = shouldUseMiyo as jest.MockedFunction<typeof shouldUseMiyo>;
const mockedGetLinkedNotes = getLinkedNotes as jest.MockedFunction<typeof getLinkedNotes>;
const mockedGetBacklinkedNotes = getBacklinkedNotes as jest.MockedFunction<
typeof getBacklinkedNotes
@ -93,7 +87,7 @@ describe("findRelevantNotes", () => {
beforeEach(() => {
jest.clearAllMocks();
mockedIsSelfHostAccessValid.mockReturnValue(false);
mockedShouldUseMiyo.mockReturnValue(false);
mockedGetSettings.mockReturnValue({
debug: false,
miyoServerUrl: "",
@ -180,7 +174,7 @@ describe("findRelevantNotes", () => {
});
it("uses Miyo when shouldUseMiyoForRelevantNotes is true (enableMiyo=true and valid self-host)", async () => {
mockedIsSelfHostAccessValid.mockReturnValue(true);
mockedShouldUseMiyo.mockReturnValue(true);
mockedGetSettings.mockReturnValue({
debug: false,
miyoServerUrl: "http://127.0.0.1:8742",
@ -256,7 +250,7 @@ describe("findRelevantNotes", () => {
});
it("falls back to link-only relevance when Miyo related-note search fails", async () => {
mockedIsSelfHostAccessValid.mockReturnValue(true);
mockedShouldUseMiyo.mockReturnValue(true);
mockedGetSettings.mockReturnValue({
debug: false,
miyoServerUrl: "http://127.0.0.1:8742",

View file

@ -1,12 +1,11 @@
import { logInfo, logWarn } from "@/logger";
import { MiyoClient } from "@/miyo/MiyoClient";
import { getMiyoCustomUrl, getMiyoVault } from "@/miyo/miyoUtils";
import { getMiyoCustomUrl, getMiyoVault, shouldUseMiyo } from "@/miyo/miyoUtils";
import { getBacklinkedNotes, getLinkedNotes } from "@/noteUtils";
import { DBOperations } from "@/search/dbOperations";
import type { SemanticIndexDocument } from "@/search/indexBackend/SemanticIndexBackend";
import VectorStoreManager from "@/search/vectorStoreManager";
import { getSettings } from "@/settings/model";
import { isSelfHostAccessValid } from "@/plusUtils";
import { InternalTypedDocument, Orama, Result } from "@orama/orama";
import { TFile } from "obsidian";
@ -20,8 +19,7 @@ const LINKS_WEIGHT = 0.3;
* @returns True when Miyo mode and self-host access validation are active.
*/
function shouldUseMiyoForRelevantNotes(): boolean {
const settings = getSettings();
return settings.enableMiyo && settings.enableSemanticSearchV3 && isSelfHostAccessValid();
return shouldUseMiyo(getSettings());
}
/**

View file

@ -4,7 +4,7 @@ import { updateIndexingProgressState } from "@/aiParams";
import { CustomError } from "@/error";
import { logInfo, logWarn } from "@/logger";
import EmbeddingsManager from "@/LLMProviders/embeddingManager";
import { isSelfHostAccessValid } from "@/plusUtils";
import { shouldUseMiyo } from "@/miyo/miyoUtils";
import { CopilotSettings, getSettings, subscribeToSettingsChange } from "@/settings/model";
import { Orama } from "@orama/orama";
import { Notice, Platform, TFile } from "obsidian";
@ -211,7 +211,7 @@ export default class VectorStoreManager {
* @returns True when Miyo should be the active backend.
*/
private shouldUseMiyo(settings: CopilotSettings): boolean {
return settings.enableMiyo && settings.enableSemanticSearchV3 && isSelfHostAccessValid();
return shouldUseMiyo(settings);
}
/**