From a0093b6392286fbd3d541e7c5c87fbf9aa86ffec Mon Sep 17 00:00:00 2001 From: yuxino Date: Sun, 27 Sep 2026 02:47:21 +0800 Subject: [PATCH 1/2] fix(browser): explain local file preview restrictions Rejected local file addresses used the generic invalid URL state, which gave no actionable explanation and could leave a blank tab without a pane state. Publish a scoped error before loading and show the workspace limit in both supported locales. Cover existing and blank tabs, preserve the file boundary, and document the user path. Refs #1094 --- apps/desktop/electron/main/browser-view.ts | 14 ++++- .../plugins/pi.browser/views/browser.html | 5 +- .../test/browser-local-file-feedback.test.mjs | 59 +++++++++++++++++++ .../test/browser-pane-navigation.test.mjs | 44 ++++++++++++++ docs/spec/04-ux/08-component-spec.md | 5 +- docs/spec/06-delivery/04-e2e-test-plan.md | 8 ++- 6 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/test/browser-local-file-feedback.test.mjs diff --git a/apps/desktop/electron/main/browser-view.ts b/apps/desktop/electron/main/browser-view.ts index 48c2af1d65..4b5e109d3d 100644 --- a/apps/desktop/electron/main/browser-view.ts +++ b/apps/desktop/electron/main/browser-view.ts @@ -118,7 +118,16 @@ export class BrowserPane { getState(): BrowserState | null { const wc = this.view?.webContents; - if (!wc || wc.isDestroyed()) return null; + if (!wc || wc.isDestroyed()) { + return this.loadError ? { + url: this.loadError.url, + title: "", + isLoading: false, + loadError: this.loadError.message, + canGoBack: false, + canGoForward: false, + } : null; + } return { url: this.loadError?.url ?? this.pendingTarget ?? wc.getURL(), title: wc.getTitle(), @@ -168,7 +177,8 @@ export class BrowserPane { : normalizeUrl(raw); if (!target) { this.beginManagedNavigation(); - this.loadError = { url: raw, message: "INVALID_URL" }; + const localInput = /^file:/i.test(raw.trim()) || isAbsolute(raw.trim()); + this.loadError = { url: raw, message: localInput ? "LOCAL_FILE_NOT_ALLOWED" : "INVALID_URL" }; const state = this.getState(); if (state) this.onState(state); return null; diff --git a/apps/desktop/resources/plugins/pi.browser/views/browser.html b/apps/desktop/resources/plugins/pi.browser/views/browser.html index 8fbc76edf1..1fd6112112 100644 --- a/apps/desktop/resources/plugins/pi.browser/views/browser.html +++ b/apps/desktop/resources/plugins/pi.browser/views/browser.html @@ -169,6 +169,7 @@

No page open

stoppedBody: "Enter an address to try again.", failedTitle: "Could not open this page", failedBody: "Check the address or connection, then try again.", + localFileBody: "Only existing files inside this project's workspace can be opened.", emptyTitle: "No page open", emptyBody: "Enter an address, or preview an HTML file from the conversation.", }, @@ -185,6 +186,7 @@

No page open

stoppedBody: "输入网址可重新打开页面。", failedTitle: "无法打开此页面", failedBody: "请检查网址或网络连接,然后重试。", + localFileBody: "只能打开此项目工作区内已存在的文件。", emptyTitle: "尚未打开页面", emptyBody: "输入地址,或从对话预览 HTML 文件。", }, @@ -232,7 +234,8 @@

No page open

empty.hidden = started && !state?.isLoading; const phase = state?.loadError === "ERR_ABORTED" ? "stopped" : state?.loadError ? "failed" : state?.isLoading ? "loading" : "empty"; emptyTitle.textContent = t(`${phase}Title`); - emptyBody.textContent = t(`${phase}Body`); + emptyBody.textContent = state?.loadError === "LOCAL_FILE_NOT_ALLOWED" + ? t("localFileBody") : t(`${phase}Body`); back.disabled = !state?.canGoBack; forward.disabled = !state?.canGoForward; reload.disabled = !started; diff --git a/apps/desktop/test/browser-local-file-feedback.test.mjs b/apps/desktop/test/browser-local-file-feedback.test.mjs new file mode 100644 index 0000000000..286e9a60c7 --- /dev/null +++ b/apps/desktop/test/browser-local-file-feedback.test.mjs @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { runInNewContext } from "node:vm"; +import test from "node:test"; + +const html = readFileSync(new URL("../resources/plugins/pi.browser/views/browser.html", import.meta.url), "utf8"); +const script = html.match(/