mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor(app): clean up find-in-pane slop
- A bridge: remove renderer webview find fallback and require the Electron bridge path - B core: tighten pane-find assertions and hide internal search/render types - C adapters: align chat/file search registration with highlightable content - D harness: delete flaky Electron QA harness and simplify keyboard dispatcher scope
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
# Electron Find QA
|
||||
|
||||
Run only the Electron browser find harness:
|
||||
|
||||
```bash
|
||||
npm run test:e2e --workspace=@getpaseo/app -- find-in-pane-electron.spec.ts --project "Desktop Chrome" --workers=1
|
||||
```
|
||||
|
||||
The spec starts a fresh desktop app from the current worktree with:
|
||||
|
||||
- per-run isolated `PASEO_HOME` under `/tmp/paseo-find-pane-electron-rerun/home-*`
|
||||
- per-run isolated Electron user data under `/tmp/paseo-find-pane-electron-rerun/electron-user-data-*`
|
||||
- `PASEO_LISTEN=127.0.0.1:0`, so it must not use port `6767`
|
||||
- a local HTTP page containing three `electronneedle` matches
|
||||
|
||||
Expected pass output: one Playwright test passes. Evidence lands in
|
||||
`/tmp/paseo-find-pane-electron-rerun/`:
|
||||
|
||||
- `electron-find-evidence.json` with timestamps, listener counts, request IDs, match events, and cleanup calls
|
||||
- `diagnostic-<timestamp>.md` with the latest run diagnosis
|
||||
- `electron-find-*.png` screenshots
|
||||
- `electron-dev.log` from the spawned desktop process
|
||||
|
||||
Failure modes:
|
||||
|
||||
- Timeout waiting for `workspace-new-browser`: the desktop app opened, but the workspace did not render tab actions.
|
||||
- LogBox screenshot after browser open: browser pane crashed before find could run.
|
||||
- Timeout waiting for `foundEvents`: `webview.findInPage()` returned a request ID, but renderer-side `found-in-page` never arrived.
|
||||
- Counter mismatch: `found-in-page` arrived, but the shared `FindBar` state did not update to the expected current/total.
|
||||
- Close/Esc mismatch: native selection or shared find cleanup did not finish.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ async function typeFindQuery(page: Page, query: string): Promise<void> {
|
||||
await input.fill(query);
|
||||
}
|
||||
|
||||
test.describe("in-pane find manual QA", () => {
|
||||
test.describe("in-pane find", () => {
|
||||
test("walks chat, file, terminal, split-pane, and browser-web find flows in the running app", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
@@ -120,7 +120,7 @@ test.describe("in-pane find manual QA", () => {
|
||||
await waitForTerminalContent(page, (text) => text.includes("split needle two"), 10_000);
|
||||
await openFind(page);
|
||||
await typeFindQuery(page, "needle");
|
||||
await expect(page.getByText(/1 \/ 2|2 \/ 2|Searching\.\.\./)).toBeVisible({
|
||||
await expect(page.getByText(/1 \/ 2|2 \/ 2/)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.getByTestId("workspace-file-pane").click();
|
||||
|
||||
@@ -40,11 +40,16 @@ interface NativeScrollToIndexFailedInfo {
|
||||
averageItemLength: number;
|
||||
}
|
||||
|
||||
interface NativeScrollIndexFallbackInput {
|
||||
index: number;
|
||||
averageItemLength: number;
|
||||
}
|
||||
|
||||
function keyExtractor(item: { id: string }): string {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
export function getNativeScrollToIndexFallbackOffset(input: NativeScrollToIndexFailedInfo) {
|
||||
export function getNativeScrollToIndexFallbackOffset(input: NativeScrollIndexFallbackInput) {
|
||||
if (!Number.isFinite(input.averageItemLength) || input.averageItemLength <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
||||
import {
|
||||
buildAgentStreamSearchModel,
|
||||
findAgentStreamSearchMatches,
|
||||
getAgentStreamItemSearchableText,
|
||||
} from "./agent-stream-search-model";
|
||||
|
||||
function timestamp(seed: number): Date {
|
||||
@@ -63,82 +61,53 @@ function todoList(id: string, seed = 1): StreamItem {
|
||||
};
|
||||
}
|
||||
|
||||
function agentToolCall(id: string, detail: ToolCallDetail, seed = 1): StreamItem {
|
||||
return {
|
||||
kind: "tool_call",
|
||||
id,
|
||||
timestamp: timestamp(seed),
|
||||
payload: {
|
||||
source: "agent",
|
||||
data: {
|
||||
provider: "codex",
|
||||
callId: `call-${id}`,
|
||||
name: "exec_command",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail,
|
||||
},
|
||||
},
|
||||
};
|
||||
function getSearchableText(item: StreamItem): string {
|
||||
const model = buildAgentStreamSearchModel({
|
||||
streamItems: [item],
|
||||
streamHead: [],
|
||||
platform: "web",
|
||||
isMobileBreakpoint: true,
|
||||
});
|
||||
return model.entries[0]?.text ?? "";
|
||||
}
|
||||
|
||||
describe("getAgentStreamItemSearchableText", () => {
|
||||
it("extracts user, assistant, thought, activity, and todo text", () => {
|
||||
expect(getAgentStreamItemSearchableText(userMessage("u1", "user text"))).toBe("user text");
|
||||
expect(getAgentStreamItemSearchableText(assistantMessage("a1", "assistant text"))).toBe(
|
||||
"assistant text",
|
||||
);
|
||||
expect(getAgentStreamItemSearchableText(thought("t1", "thought text"))).toBe("thought text");
|
||||
expect(getAgentStreamItemSearchableText(activityLog("l1", "activity text"))).toBe(
|
||||
"activity text",
|
||||
);
|
||||
expect(getAgentStreamItemSearchableText(todoList("todo"))).toBe(
|
||||
"Write the red test\nMake search green",
|
||||
);
|
||||
describe("buildAgentStreamSearchModel", () => {
|
||||
it("indexes user and assistant message text", () => {
|
||||
expect(getSearchableText(userMessage("u1", "user text"))).toBe("user text");
|
||||
expect(getSearchableText(assistantMessage("a1", "assistant text"))).toBe("assistant text");
|
||||
});
|
||||
|
||||
it("searches minimal visible tool-call text and skips raw hidden payloads", () => {
|
||||
const shell = agentToolCall("shell", {
|
||||
type: "shell",
|
||||
command: "npm run typecheck",
|
||||
output: "internal output should stay out",
|
||||
});
|
||||
|
||||
expect(getAgentStreamItemSearchableText(shell)).toBe("Shell\nnpm run typecheck");
|
||||
it("excludes non-message rows that do not render find highlights", () => {
|
||||
expect(getSearchableText(thought("t1", "thought text"))).toBe("");
|
||||
expect(getSearchableText(activityLog("l1", "activity text"))).toBe("");
|
||||
expect(getSearchableText(todoList("todo"))).toBe("");
|
||||
});
|
||||
|
||||
it("includes special tool-call content branches that render as messages or cards", () => {
|
||||
const speak: StreamItem = {
|
||||
it("excludes tool call rows from the search index", () => {
|
||||
const toolCall: StreamItem = {
|
||||
kind: "tool_call",
|
||||
id: "speak",
|
||||
id: "shell",
|
||||
timestamp: timestamp(1),
|
||||
payload: {
|
||||
source: "agent",
|
||||
data: {
|
||||
provider: "codex",
|
||||
callId: "call-speak",
|
||||
name: "speak",
|
||||
callId: "call-shell",
|
||||
name: "exec_command",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: "spoken message",
|
||||
output: null,
|
||||
type: "shell",
|
||||
command: "npm run typecheck",
|
||||
output: "internal output should stay out",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const plan = agentToolCall("plan", {
|
||||
type: "plan",
|
||||
text: "phase checklist",
|
||||
});
|
||||
|
||||
expect(getAgentStreamItemSearchableText(speak)).toBe("spoken message");
|
||||
expect(getAgentStreamItemSearchableText(plan)).toBe("Plan\nphase checklist");
|
||||
expect(getSearchableText(toolCall)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAgentStreamSearchModel", () => {
|
||||
it("orders virtualized history, mounted history, live head, and optimistic items deterministically", () => {
|
||||
const committed: StreamItem[] = [];
|
||||
for (let index = 0; index < 64; index += 1) {
|
||||
@@ -185,7 +154,7 @@ describe("findAgentStreamSearchMatches", () => {
|
||||
it("returns stable match ids from item identity and local occurrence data", () => {
|
||||
const model = buildAgentStreamSearchModel({
|
||||
streamItems: [assistantMessage("a1", "Alpha alpha beta")],
|
||||
streamHead: [thought("h1", "alpha live")],
|
||||
streamHead: [assistantMessage("h1", "alpha live")],
|
||||
platform: "web",
|
||||
isMobileBreakpoint: true,
|
||||
});
|
||||
@@ -202,4 +171,20 @@ describe("findAgentStreamSearchMatches", () => {
|
||||
]);
|
||||
expect(matches.map((match) => match.entry.item.id)).toEqual(["a1", "a1", "h1"]);
|
||||
});
|
||||
|
||||
it("skips fenced code blocks while preserving message offsets for highlights", () => {
|
||||
const model = buildAgentStreamSearchModel({
|
||||
streamItems: [assistantMessage("a1", "before alpha\n```\nalpha\n```\nafter alpha")],
|
||||
streamHead: [],
|
||||
platform: "web",
|
||||
isMobileBreakpoint: true,
|
||||
});
|
||||
|
||||
const matches = findAgentStreamSearchMatches({
|
||||
model,
|
||||
query: "alpha",
|
||||
});
|
||||
|
||||
expect(matches.map((match) => match.id)).toEqual(["a1:text:0:7:12", "a1:text:0:33:38"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { StreamItem, ToolCallItem } from "@/types/stream";
|
||||
import { buildToolCallDisplayModel } from "@/utils/tool-call-display";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import {
|
||||
findMountedWindowStart,
|
||||
getWebMountedRecentStreamItems,
|
||||
getWebPartialVirtualizationThreshold,
|
||||
} from "./agent-stream-web-virtualization";
|
||||
|
||||
export type AgentStreamSearchSource = "historyVirtualized" | "historyMounted" | "liveHead";
|
||||
type AgentStreamSearchSource = "historyVirtualized" | "historyMounted" | "liveHead";
|
||||
|
||||
export interface AgentStreamSearchTextSegment {
|
||||
interface AgentStreamSearchTextSegment {
|
||||
key: string;
|
||||
text: string;
|
||||
startOffset: number;
|
||||
}
|
||||
|
||||
export interface AgentStreamSearchEntry {
|
||||
interface AgentStreamSearchEntry {
|
||||
item: StreamItem;
|
||||
source: AgentStreamSearchSource;
|
||||
index: number;
|
||||
@@ -30,7 +30,7 @@ export interface AgentStreamSearchMatch {
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface AgentStreamSearchModel {
|
||||
interface AgentStreamSearchModel {
|
||||
entries: AgentStreamSearchEntry[];
|
||||
segments: {
|
||||
historyVirtualized: AgentStreamSearchEntry[];
|
||||
@@ -39,7 +39,7 @@ export interface AgentStreamSearchModel {
|
||||
};
|
||||
}
|
||||
|
||||
export interface BuildAgentStreamSearchModelInput {
|
||||
interface BuildAgentStreamSearchModelInput {
|
||||
platform: "web" | "native";
|
||||
isMobileBreakpoint: boolean;
|
||||
streamItems: StreamItem[];
|
||||
@@ -48,99 +48,79 @@ export interface BuildAgentStreamSearchModelInput {
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export interface FindAgentStreamSearchMatchesInput {
|
||||
interface FindAgentStreamSearchMatchesInput {
|
||||
model: AgentStreamSearchModel;
|
||||
query: string;
|
||||
}
|
||||
|
||||
function compactText(parts: Array<string | undefined>): string {
|
||||
return parts
|
||||
.map((part) => part?.trim())
|
||||
.filter((part): part is string => Boolean(part))
|
||||
.join("\n");
|
||||
function getFenceDelimiter(line: string): string | null {
|
||||
const match = /^( {0,3})(`{3,}|~{3,})/.exec(line);
|
||||
return match?.[2] ?? null;
|
||||
}
|
||||
|
||||
function getToolCallSearchableSegments(
|
||||
item: ToolCallItem,
|
||||
cwd: string | undefined,
|
||||
): AgentStreamSearchTextSegment[] {
|
||||
if (item.payload.source === "agent") {
|
||||
const { data } = item.payload;
|
||||
if (
|
||||
data.name === "speak" &&
|
||||
data.detail.type === "unknown" &&
|
||||
typeof data.detail.input === "string" &&
|
||||
data.detail.input.trim()
|
||||
) {
|
||||
return [{ key: "text", text: data.detail.input }];
|
||||
function getMessageSearchableSegments(text: string): AgentStreamSearchTextSegment[] {
|
||||
const segments: AgentStreamSearchTextSegment[] = [];
|
||||
let activeFenceCharacter: "`" | "~" | null = null;
|
||||
let activeFenceLength = 0;
|
||||
let currentText = "";
|
||||
let currentStartOffset = 0;
|
||||
let offset = 0;
|
||||
|
||||
const flush = () => {
|
||||
if (currentText.length > 0) {
|
||||
segments.push({ key: "text", text: currentText, startOffset: currentStartOffset });
|
||||
currentText = "";
|
||||
}
|
||||
};
|
||||
|
||||
for (const line of text.split("\n")) {
|
||||
const lineWithBreak = offset + line.length < text.length ? `${line}\n` : line;
|
||||
const fenceDelimiter = getFenceDelimiter(line);
|
||||
const isClosingFence =
|
||||
activeFenceCharacter &&
|
||||
fenceDelimiter?.[0] === activeFenceCharacter &&
|
||||
fenceDelimiter.length >= activeFenceLength;
|
||||
const isOpeningFence = !activeFenceCharacter && fenceDelimiter;
|
||||
const isIndentedCode = !activeFenceCharacter && (/^( {4,}|\t)/.test(line) || line === " ");
|
||||
|
||||
if (isOpeningFence || activeFenceCharacter || isIndentedCode) {
|
||||
flush();
|
||||
} else {
|
||||
if (currentText.length === 0) {
|
||||
currentStartOffset = offset;
|
||||
}
|
||||
currentText += lineWithBreak;
|
||||
}
|
||||
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: data.name,
|
||||
status: data.status,
|
||||
error: data.error,
|
||||
detail: data.detail,
|
||||
metadata: data.metadata,
|
||||
cwd,
|
||||
});
|
||||
const visibleText = compactText([
|
||||
display.displayName,
|
||||
display.summary,
|
||||
data.detail.type === "plan" ? data.detail.text : undefined,
|
||||
display.errorText,
|
||||
]);
|
||||
return visibleText ? [{ key: "tool", text: visibleText }] : [];
|
||||
if (isOpeningFence) {
|
||||
activeFenceCharacter = fenceDelimiter[0] as "`" | "~";
|
||||
activeFenceLength = fenceDelimiter.length;
|
||||
} else if (isClosingFence) {
|
||||
activeFenceCharacter = null;
|
||||
activeFenceLength = 0;
|
||||
}
|
||||
|
||||
offset += lineWithBreak.length;
|
||||
}
|
||||
|
||||
const { data } = item.payload;
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: data.toolName,
|
||||
status: data.status === "executing" ? "running" : data.status,
|
||||
error: data.error,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: data.arguments,
|
||||
output: data.result ?? null,
|
||||
},
|
||||
cwd,
|
||||
});
|
||||
const visibleText = compactText([display.displayName, display.summary, display.errorText]);
|
||||
return visibleText ? [{ key: "tool", text: visibleText }] : [];
|
||||
flush();
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function getAgentStreamItemSearchableSegments(
|
||||
item: StreamItem,
|
||||
options: { cwd?: string } = {},
|
||||
): AgentStreamSearchTextSegment[] {
|
||||
function getAgentStreamItemSearchableSegments(item: StreamItem): AgentStreamSearchTextSegment[] {
|
||||
switch (item.kind) {
|
||||
case "user_message":
|
||||
case "assistant_message":
|
||||
return item.text ? getMessageSearchableSegments(item.text) : [];
|
||||
case "thought":
|
||||
return item.text ? [{ key: "text", text: item.text }] : [];
|
||||
case "activity_log":
|
||||
return item.message ? [{ key: "text", text: item.message }] : [];
|
||||
case "todo_list":
|
||||
return item.items.map((todo, index) => ({
|
||||
key: `todo:${index}`,
|
||||
text: todo.text,
|
||||
}));
|
||||
case "tool_call":
|
||||
return getToolCallSearchableSegments(item, options.cwd);
|
||||
case "compaction":
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgentStreamItemSearchableText(
|
||||
item: StreamItem,
|
||||
options: { cwd?: string } = {},
|
||||
): string {
|
||||
return getAgentStreamItemSearchableSegments(item, options)
|
||||
.map((segment) => segment.text)
|
||||
.filter((text) => text.length > 0)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function mergeOptimisticItems(input: {
|
||||
streamItems: StreamItem[];
|
||||
optimisticItems: StreamItem[] | undefined;
|
||||
@@ -163,7 +143,7 @@ function buildEntries(input: {
|
||||
cwd: string | undefined;
|
||||
}): AgentStreamSearchEntry[] {
|
||||
return input.items.map((item, offset) => {
|
||||
const segments = getAgentStreamItemSearchableSegments(item, { cwd: input.cwd });
|
||||
const segments = getAgentStreamItemSearchableSegments(item);
|
||||
return {
|
||||
item,
|
||||
source: input.source,
|
||||
@@ -277,13 +257,15 @@ export function findAgentStreamSearchMatches(
|
||||
break;
|
||||
}
|
||||
const end = start + input.query.length;
|
||||
const absoluteStart = segment.startOffset + start;
|
||||
const absoluteEnd = segment.startOffset + end;
|
||||
matches.push({
|
||||
id: `${entry.item.id}:${segment.key}:${occurrenceIndex}:${start}:${end}`,
|
||||
id: `${entry.item.id}:${segment.key}:${occurrenceIndex}:${absoluteStart}:${absoluteEnd}`,
|
||||
entry,
|
||||
segmentKey: segment.key,
|
||||
occurrenceIndex,
|
||||
start,
|
||||
end,
|
||||
start: absoluteStart,
|
||||
end: absoluteEnd,
|
||||
});
|
||||
occurrenceIndex += 1;
|
||||
fromIndex = end;
|
||||
|
||||
@@ -207,8 +207,6 @@ vi.mock("@/stores/browser-store", () => ({
|
||||
}));
|
||||
|
||||
type FakeWebview = HTMLDivElement & {
|
||||
findInPage: ReturnType<typeof vi.fn<(text: string, options?: unknown) => number>>;
|
||||
stopFindInPage: ReturnType<typeof vi.fn<(action: string) => void>>;
|
||||
getURL: ReturnType<typeof vi.fn<() => string>>;
|
||||
canGoBack: ReturnType<typeof vi.fn<() => boolean>>;
|
||||
canGoForward: ReturnType<typeof vi.fn<() => boolean>>;
|
||||
@@ -248,8 +246,6 @@ function installWebviewElementFactory(): void {
|
||||
return originalCreateElement(tagName, options);
|
||||
}
|
||||
const element = originalCreateElement("div") as FakeWebview;
|
||||
element.findInPage = vi.fn(() => nextRequestId++);
|
||||
element.stopFindInPage = vi.fn();
|
||||
element.getURL = vi.fn(() => "https://example.com");
|
||||
element.canGoBack = vi.fn(() => false);
|
||||
element.canGoForward = vi.fn(() => false);
|
||||
@@ -423,23 +419,6 @@ describe("BrowserPane Electron find", () => {
|
||||
expect(container?.querySelector('[data-testid="pane-find-input"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores requestId-less found-in-page events while a newer requestId search is active", () => {
|
||||
renderBrowserPane();
|
||||
markWebviewDomReady();
|
||||
openFind();
|
||||
|
||||
changeInput("needle");
|
||||
dispatchFoundInPage({ requestId: 1, activeMatchOrdinal: 1, matches: 2 });
|
||||
expect(container?.textContent).toContain("1 / 2");
|
||||
|
||||
pressKey("Enter");
|
||||
dispatchFoundInPage({ activeMatchOrdinal: 2, matches: 9 });
|
||||
expect(container?.textContent).toContain("Searching...");
|
||||
|
||||
dispatchFoundInPage({ requestId: 2, activeMatchOrdinal: 2, matches: 2 });
|
||||
expect(container?.textContent).toContain("2 / 2");
|
||||
});
|
||||
|
||||
it("cleans browser find selection on empty query, navigation, blur, and unmount", () => {
|
||||
renderBrowserPane();
|
||||
markWebviewDomReady();
|
||||
@@ -480,7 +459,7 @@ describe("BrowserPane Electron find", () => {
|
||||
expect(desktopBridge.foundInPageListeners.size).toBe(0);
|
||||
});
|
||||
|
||||
it("does not call webview find methods before dom-ready", () => {
|
||||
it("does not call the browser find bridge before dom-ready", () => {
|
||||
renderBrowserPane();
|
||||
openFind();
|
||||
|
||||
|
||||
@@ -28,12 +28,6 @@ import { isDev } from "@/constants/platform";
|
||||
import { FindBar, usePaneFind, type PaneFindMatchState } from "@/panels/pane-find";
|
||||
import { useBrowserStore, normalizeWorkspaceBrowserUrl } from "@/stores/browser-store";
|
||||
|
||||
interface ElectronFindOptions {
|
||||
forward?: boolean;
|
||||
findNext?: boolean;
|
||||
matchCase?: boolean;
|
||||
}
|
||||
|
||||
type ElectronWebview = HTMLElement & {
|
||||
canGoBack?: () => boolean;
|
||||
canGoForward?: () => boolean;
|
||||
@@ -44,20 +38,11 @@ type ElectronWebview = HTMLElement & {
|
||||
loadURL?: (url: string) => Promise<void>;
|
||||
getURL?: () => string;
|
||||
executeJavaScript?: (code: string) => Promise<unknown>;
|
||||
findInPage?: (text: string, options?: ElectronFindOptions) => number;
|
||||
stopFindInPage?: (action: "clearSelection" | "keepSelection" | "activateSelection") => void;
|
||||
focus?: () => void;
|
||||
addEventListener: (type: string, listener: EventListenerOrEventListenerObject) => void;
|
||||
removeEventListener: (type: string, listener: EventListenerOrEventListenerObject) => void;
|
||||
};
|
||||
|
||||
interface ElectronFoundInPageResult extends DesktopBrowserFoundInPageResult {
|
||||
requestId?: number;
|
||||
activeMatchOrdinal?: number;
|
||||
matches?: number;
|
||||
finalUpdate?: boolean;
|
||||
}
|
||||
|
||||
type WebTextInput = TextInput & {
|
||||
getNativeRef?: () => unknown;
|
||||
};
|
||||
@@ -261,25 +246,16 @@ function isDesktopBrowserShortcutEvent(payload: unknown): payload is DesktopBrow
|
||||
return event.action === "focus-url";
|
||||
}
|
||||
|
||||
function getFoundInPageResult(event: Event): ElectronFoundInPageResult | null {
|
||||
const result = (event as Event & { result?: unknown }).result;
|
||||
if (!result || typeof result !== "object") {
|
||||
return null;
|
||||
}
|
||||
return result as ElectronFoundInPageResult;
|
||||
}
|
||||
|
||||
function stopBrowserFindInPage(input: {
|
||||
browserId: string;
|
||||
webview: ElectronWebview | null;
|
||||
action: DesktopBrowserFindAction;
|
||||
}): void {
|
||||
const bridge = getDesktopHost()?.browser;
|
||||
if (bridge?.stopFindInPage) {
|
||||
void bridge.stopFindInPage(input.browserId, input.action);
|
||||
if (!bridge?.stopFindInPage) {
|
||||
console.warn("Electron browser find bridge is unavailable; cannot stop find-in-page.");
|
||||
return;
|
||||
}
|
||||
input.webview?.stopFindInPage?.(input.action);
|
||||
void bridge.stopFindInPage(input.browserId, input.action);
|
||||
}
|
||||
|
||||
function startSelectorResultPolling(input: {
|
||||
@@ -436,7 +412,6 @@ export function BrowserPane({
|
||||
if (domReadyRef.current) {
|
||||
stopBrowserFindInPage({
|
||||
browserId: browserIdRef.current,
|
||||
webview: webviewRef.current,
|
||||
action: "clearSelection",
|
||||
});
|
||||
}
|
||||
@@ -450,13 +425,13 @@ export function BrowserPane({
|
||||
return;
|
||||
}
|
||||
|
||||
const webview = webviewRef.current;
|
||||
if (!domReadyRef.current) {
|
||||
setBrowserFindMatchState(PENDING_FIND_MATCH_STATE);
|
||||
return;
|
||||
}
|
||||
const bridgeFindInPage = getDesktopHost()?.browser?.findInPage;
|
||||
if (!bridgeFindInPage && !webview?.findInPage) {
|
||||
if (!bridgeFindInPage) {
|
||||
console.warn("Electron browser find bridge is unavailable; cannot start find-in-page.");
|
||||
setBrowserFindMatchState(NO_FIND_MATCH_STATE);
|
||||
return;
|
||||
}
|
||||
@@ -474,9 +449,7 @@ export function BrowserPane({
|
||||
findNext: !input?.reset,
|
||||
matchCase: false,
|
||||
};
|
||||
const requestIdResult = bridgeFindInPage
|
||||
? bridgeFindInPage(browserIdRef.current, query, options)
|
||||
: webview?.findInPage?.(query, options);
|
||||
const requestIdResult = bridgeFindInPage(browserIdRef.current, query, options);
|
||||
if (typeof requestIdResult === "number") {
|
||||
activeBrowserFindRef.current = {
|
||||
generation,
|
||||
@@ -504,7 +477,7 @@ export function BrowserPane({
|
||||
[clearBrowserFindSelection],
|
||||
);
|
||||
|
||||
const handleFoundInPageResult = useCallback((result: ElectronFoundInPageResult) => {
|
||||
const handleFoundInPageResult = useCallback((result: DesktopBrowserFoundInPageResult) => {
|
||||
const activeFind = activeBrowserFindRef.current;
|
||||
if (
|
||||
!activeFind ||
|
||||
@@ -513,12 +486,11 @@ export function BrowserPane({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const eventRequestId = typeof result.requestId === "number" ? result.requestId : null;
|
||||
if (typeof activeFind.requestId === "number") {
|
||||
if (eventRequestId !== activeFind.requestId) {
|
||||
return;
|
||||
}
|
||||
} else if (eventRequestId !== null) {
|
||||
const requestId = result.requestId;
|
||||
if (typeof requestId !== "number") {
|
||||
return;
|
||||
}
|
||||
if (typeof activeFind.requestId !== "number" || requestId !== activeFind.requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -539,17 +511,6 @@ export function BrowserPane({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleFoundInPage = useCallback(
|
||||
(event: Event) => {
|
||||
const result = getFoundInPageResult(event);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
handleFoundInPageResult(result);
|
||||
},
|
||||
[handleFoundInPageResult],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectronRuntime()) {
|
||||
return;
|
||||
@@ -691,7 +652,7 @@ export function BrowserPane({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
webview.addEventListener("found-in-page", handleFoundInPage);
|
||||
console.warn("Electron browser find bridge is unavailable; found-in-page events disabled.");
|
||||
}
|
||||
|
||||
host.appendChild(webview);
|
||||
@@ -714,15 +675,11 @@ export function BrowserPane({
|
||||
webview.removeEventListener("dom-ready", handleDomReady);
|
||||
didCleanupFoundInPage = true;
|
||||
unsubscribeFoundInPage?.();
|
||||
if (!foundInPageBridge) {
|
||||
webview.removeEventListener("found-in-page", handleFoundInPage);
|
||||
}
|
||||
webview.removeEventListener("focus", handleWebviewFocus);
|
||||
webview.removeEventListener("mousedown", handleWebviewFocus);
|
||||
if (domReadyRef.current) {
|
||||
stopBrowserFindInPage({
|
||||
browserId: browserIdRef.current,
|
||||
webview,
|
||||
action: "clearSelection",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
createFilePaneTextRenderData,
|
||||
findFilePaneTextMatches,
|
||||
} from "@/components/file-pane-text-render-data";
|
||||
import type { FilePaneTextLineRenderData } from "@/components/file-pane-text-render-data";
|
||||
|
||||
type FilePaneTextLineRenderData = ReturnType<typeof createFilePaneTextRenderData>["lines"][number];
|
||||
|
||||
function tokenText(line: FilePaneTextLineRenderData): string {
|
||||
return line.tokens.map(({ text }) => text).join("");
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { highlightCode, type HighlightToken } from "@getpaseo/highlight";
|
||||
|
||||
export interface FilePaneTextLineRenderData {
|
||||
interface FilePaneTextLineRenderData {
|
||||
lineNumber: number;
|
||||
text: string;
|
||||
tokens: HighlightToken[];
|
||||
}
|
||||
|
||||
export interface FilePaneTextRenderData {
|
||||
interface FilePaneTextRenderData {
|
||||
lines: FilePaneTextLineRenderData[];
|
||||
searchableText: string;
|
||||
}
|
||||
|
||||
export interface FilePaneFindLineSpan {
|
||||
interface FilePaneFindLineSpan {
|
||||
lineNumber: number;
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
|
||||
@@ -93,6 +93,13 @@ interface FilePaneTextPreviewProps {
|
||||
webScrollbarStyle: object;
|
||||
}
|
||||
|
||||
interface FilePaneSearchableTextPreviewProps extends Omit<
|
||||
FilePaneTextPreviewProps,
|
||||
"findHighlightsByLine" | "textRenderData"
|
||||
> {
|
||||
textRenderData: ReturnType<typeof createFilePaneTextRenderData>;
|
||||
}
|
||||
|
||||
interface FilePaneImagePreviewProps {
|
||||
imagePreviewUri: string | null;
|
||||
imageSource: { uri: string } | null;
|
||||
@@ -598,6 +605,20 @@ function FilePaneTextPreview({
|
||||
);
|
||||
}
|
||||
|
||||
function FilePaneSearchableTextPreview(props: FilePaneSearchableTextPreviewProps) {
|
||||
const { findHighlightsByLine, paneFind } = useFilePaneFindAdapter({
|
||||
textRenderData: props.textRenderData,
|
||||
textScrollRefs: props.textScrollRefs,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilePaneFindBarSlot paneFind={paneFind} />
|
||||
<FilePaneTextPreview {...props} findHighlightsByLine={findHighlightsByLine} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FilePaneImagePreview({
|
||||
imagePreviewUri,
|
||||
imageSource,
|
||||
@@ -676,11 +697,6 @@ function FilePreviewBody({
|
||||
const scrollbar = useWebScrollViewScrollbar(previewScrollRef, {
|
||||
enabled: showDesktopWebScrollbar,
|
||||
});
|
||||
const { findHighlightsByLine, paneFind } = useFilePaneFindAdapter({
|
||||
textRenderData,
|
||||
textScrollRefs,
|
||||
});
|
||||
|
||||
const gutterWidth = useMemo(() => {
|
||||
if (!textRenderData) return 0;
|
||||
return lineNumberGutterWidth(textRenderData.lines.length, theme.fontSize.code);
|
||||
@@ -729,11 +745,32 @@ function FilePreviewBody({
|
||||
<Text style={styles.emptyText}>No preview available</Text>
|
||||
</FilePaneCenterState>
|
||||
);
|
||||
} else if (preview.kind === "text" && textRenderData) {
|
||||
content = (
|
||||
<FilePaneSearchableTextPreview
|
||||
baseColor={baseColor}
|
||||
colorMap={colorMap}
|
||||
currentMatchBackgroundColor={currentMatchBackgroundColor}
|
||||
gutterWidth={gutterWidth}
|
||||
isMarkdownFile={isMarkdownFile}
|
||||
isMobile={isMobile}
|
||||
markdownParser={markdownParser}
|
||||
markdownStyles={markdownStyles}
|
||||
matchBackgroundColor={matchBackgroundColor}
|
||||
preview={preview}
|
||||
previewScrollRef={previewScrollRef}
|
||||
scrollbar={scrollbar}
|
||||
showDesktopWebScrollbar={showDesktopWebScrollbar}
|
||||
textRenderData={textRenderData}
|
||||
textScrollRefs={textScrollRefs}
|
||||
webScrollbarStyle={webScrollbarStyle}
|
||||
/>
|
||||
);
|
||||
} else if (preview.kind === "text") {
|
||||
content = (
|
||||
<FilePaneTextPreview
|
||||
currentMatchBackgroundColor={currentMatchBackgroundColor}
|
||||
findHighlightsByLine={findHighlightsByLine}
|
||||
findHighlightsByLine={new Map()}
|
||||
gutterWidth={gutterWidth}
|
||||
isMarkdownFile={isMarkdownFile}
|
||||
isMobile={isMobile}
|
||||
@@ -769,12 +806,7 @@ function FilePreviewBody({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.previewScrollContainer}>
|
||||
<FilePaneFindBarSlot paneFind={paneFind} />
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
return <View style={styles.previewScrollContainer}>{content}</View>;
|
||||
}
|
||||
|
||||
export function FilePane({
|
||||
|
||||
@@ -241,7 +241,7 @@ function normalizeFindHighlights(
|
||||
.sort((left, right) => left.start - right.start || left.end - right.end);
|
||||
}
|
||||
|
||||
export function createMessageFindTextSegments(
|
||||
function createMessageFindTextSegments(
|
||||
text: string,
|
||||
highlights: MessageFindHighlight[] | undefined,
|
||||
): MessageFindTextSegment[] {
|
||||
@@ -283,7 +283,7 @@ interface MarkdownBlockWithOffset {
|
||||
startOffset: number;
|
||||
}
|
||||
|
||||
export function createMarkdownBlocksWithOffsets(message: string): MarkdownBlockWithOffset[] {
|
||||
function createMarkdownBlocksWithOffsets(message: string): MarkdownBlockWithOffset[] {
|
||||
let cursor = 0;
|
||||
return splitMarkdownBlocks(message).map((block, index) => {
|
||||
const startOffset = Math.max(cursor, message.indexOf(block, cursor));
|
||||
|
||||
@@ -6,7 +6,6 @@ describe("getNativeScrollToIndexFallbackOffset", () => {
|
||||
expect(
|
||||
getNativeScrollToIndexFallbackOffset({
|
||||
index: 25,
|
||||
highestMeasuredFrameIndex: 4,
|
||||
averageItemLength: 72,
|
||||
}),
|
||||
).toBe(1800);
|
||||
@@ -16,14 +15,12 @@ describe("getNativeScrollToIndexFallbackOffset", () => {
|
||||
expect(
|
||||
getNativeScrollToIndexFallbackOffset({
|
||||
index: 25,
|
||||
highestMeasuredFrameIndex: 4,
|
||||
averageItemLength: 0,
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(
|
||||
getNativeScrollToIndexFallbackOffset({
|
||||
index: 25,
|
||||
highestMeasuredFrameIndex: 4,
|
||||
averageItemLength: Number.NaN,
|
||||
}),
|
||||
).toBe(0);
|
||||
|
||||
@@ -111,13 +111,13 @@ export interface DesktopBrowserBridge {
|
||||
setWorkspaceActiveBrowser?: (browserId: string | null) => Promise<void>;
|
||||
openDevTools?: (browserId: string) => Promise<unknown>;
|
||||
clearPartition?: (browserId: string) => Promise<void>;
|
||||
findInPage?: (
|
||||
findInPage: (
|
||||
browserId: string,
|
||||
text: string,
|
||||
options?: DesktopBrowserFindOptions,
|
||||
) => Promise<number | null> | number | null;
|
||||
stopFindInPage?: (browserId: string, action: DesktopBrowserFindAction) => Promise<void> | void;
|
||||
onFoundInPage?: (
|
||||
stopFindInPage: (browserId: string, action: DesktopBrowserFindAction) => Promise<void> | void;
|
||||
onFoundInPage: (
|
||||
browserId: string,
|
||||
listener: (result: DesktopBrowserFoundInPageResult) => void,
|
||||
) => Promise<() => void> | (() => void);
|
||||
|
||||
@@ -11,7 +11,6 @@ export type KeyboardActionId =
|
||||
| "message-input.voice-mute-toggle"
|
||||
| "workspace.tab.new"
|
||||
| "workspace.tab.close-current"
|
||||
| "workspace.find.open"
|
||||
| "workspace.tab.navigate-index"
|
||||
| "workspace.tab.navigate-relative"
|
||||
| "workspace.pane.split.right"
|
||||
@@ -41,7 +40,6 @@ export type KeyboardActionDefinition =
|
||||
| { id: "message-input.voice-mute-toggle"; scope: KeyboardActionScope }
|
||||
| { id: "workspace.tab.new"; scope: KeyboardActionScope }
|
||||
| { id: "workspace.tab.close-current"; scope: KeyboardActionScope }
|
||||
| { id: "workspace.find.open"; scope: KeyboardActionScope }
|
||||
| { id: "workspace.tab.navigate-index"; scope: KeyboardActionScope; index: number }
|
||||
| { id: "workspace.tab.navigate-relative"; scope: KeyboardActionScope; delta: 1 | -1 }
|
||||
| { id: "workspace.pane.split.right"; scope: KeyboardActionScope }
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
type PaneFindController,
|
||||
} from "@/panels/pane-find-registry";
|
||||
|
||||
function createController(): PaneFindController {
|
||||
function createController(input?: { openResult?: boolean }): PaneFindController {
|
||||
return {
|
||||
openFind: vi.fn(() => true),
|
||||
openFind: vi.fn(() => input?.openResult ?? true),
|
||||
closeFind: vi.fn(() => true),
|
||||
};
|
||||
}
|
||||
@@ -22,8 +22,8 @@ describe("pane find registry", () => {
|
||||
const registry = createPaneFindRegistry({
|
||||
getActivePaneId: () => activePaneId.current,
|
||||
});
|
||||
const left = createController();
|
||||
const right = createController();
|
||||
const left = createController({ openResult: false });
|
||||
const right = createController({ openResult: true });
|
||||
|
||||
registry.register({
|
||||
paneId: "server:workspace:left",
|
||||
@@ -34,9 +34,7 @@ describe("pane find registry", () => {
|
||||
controller: right,
|
||||
});
|
||||
|
||||
expect(registry.openFindInActivePane()).toBe(true);
|
||||
expect(left.openFind).toHaveBeenCalledTimes(1);
|
||||
expect(right.openFind).not.toHaveBeenCalled();
|
||||
expect(registry.openFindInActivePane()).toBe(false);
|
||||
});
|
||||
|
||||
it("stops routing to a pane after it unregisters", () => {
|
||||
@@ -52,7 +50,6 @@ describe("pane find registry", () => {
|
||||
unregister();
|
||||
|
||||
expect(registry.openFindInActivePane()).toBe(false);
|
||||
expect(controller.openFind).not.toHaveBeenCalled();
|
||||
expect(controller.closeFind).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -61,8 +58,8 @@ describe("pane find registry", () => {
|
||||
const registry = createPaneFindRegistry({
|
||||
getActivePaneId: () => activePaneId.current,
|
||||
});
|
||||
const left = createController();
|
||||
const right = createController();
|
||||
const left = createController({ openResult: false });
|
||||
const right = createController({ openResult: true });
|
||||
|
||||
registry.register({
|
||||
paneId: "server:workspace:left",
|
||||
@@ -74,8 +71,6 @@ describe("pane find registry", () => {
|
||||
});
|
||||
|
||||
expect(registry.openFindInActivePane()).toBe(true);
|
||||
expect(left.openFind).not.toHaveBeenCalled();
|
||||
expect(right.openFind).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handles the keyboard find action through the active pane", () => {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||
|
||||
export interface PaneFindController {
|
||||
openFind(): boolean;
|
||||
closeFind(): boolean;
|
||||
}
|
||||
|
||||
export interface RegisterPaneFindInput {
|
||||
interface PaneFindKeyboardAction {
|
||||
id: "workspace.find.open";
|
||||
scope: "workspace";
|
||||
}
|
||||
|
||||
interface RegisterPaneFindInput {
|
||||
paneId: string;
|
||||
controller: PaneFindController;
|
||||
}
|
||||
|
||||
export interface PaneFindRegistry {
|
||||
interface PaneFindRegistry {
|
||||
register(input: RegisterPaneFindInput): () => void;
|
||||
openFindInActivePane(): boolean;
|
||||
closeFindInPane(paneId: string): boolean;
|
||||
@@ -73,9 +76,7 @@ export function clearActivePaneFindPaneId(paneId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePaneFindKeyboardAction(action: KeyboardActionDefinition): boolean {
|
||||
if (action.id !== "workspace.find.open") {
|
||||
return false;
|
||||
}
|
||||
export function handlePaneFindKeyboardAction(action: PaneFindKeyboardAction): boolean {
|
||||
void action;
|
||||
return paneFindRegistry.openFindInActivePane();
|
||||
}
|
||||
|
||||
@@ -145,13 +145,14 @@ interface FakeSearchController {
|
||||
|
||||
const controllers = new Map<string, FakeSearchController>();
|
||||
|
||||
function createController(): FakeSearchController {
|
||||
function createController(input?: { total?: number }): FakeSearchController {
|
||||
const total = input?.total ?? 3;
|
||||
return {
|
||||
query: vi.fn((query: string) =>
|
||||
query === "missing" ? { status: "no-match" } : { status: "matched", current: 1, total: 3 },
|
||||
query === "missing" ? { status: "no-match" } : { status: "matched", current: 1, total },
|
||||
),
|
||||
next: vi.fn(() => ({ status: "matched", current: 2, total: 3 })),
|
||||
prev: vi.fn(() => ({ status: "matched", current: 3, total: 3 })),
|
||||
next: vi.fn(() => ({ status: "matched", current: 2, total })),
|
||||
prev: vi.fn(() => ({ status: "matched", current: 3, total })),
|
||||
close: vi.fn(),
|
||||
};
|
||||
}
|
||||
@@ -159,11 +160,14 @@ function createController(): FakeSearchController {
|
||||
function FakeFindPanel() {
|
||||
const paneContext = usePaneContext();
|
||||
const controller = controllers.get(paneContext.paneInstanceId ?? "");
|
||||
if (!controller) {
|
||||
throw new Error(`Missing fake find controller for pane ${paneContext.paneInstanceId}`);
|
||||
}
|
||||
const paneFind = usePaneFind({
|
||||
onQuery: controller?.query ?? createController().query,
|
||||
onNext: controller?.next ?? createController().next,
|
||||
onPrev: controller?.prev ?? createController().prev,
|
||||
onClose: controller?.close ?? createController().close,
|
||||
onQuery: controller.query,
|
||||
onNext: controller.next,
|
||||
onPrev: controller.prev,
|
||||
onClose: controller.close,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -350,12 +354,12 @@ describe("FindBar", () => {
|
||||
act(() => {
|
||||
button("pane-find-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
expect(controller.next).toHaveBeenCalledTimes(2);
|
||||
expect(container?.textContent).toContain("2 / 3");
|
||||
|
||||
act(() => {
|
||||
button("pane-find-prev").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
expect(controller.prev).toHaveBeenCalledTimes(2);
|
||||
expect(container?.textContent).toContain("3 / 3");
|
||||
|
||||
pressKey("Escape");
|
||||
expect(controller.close).toHaveBeenCalledTimes(1);
|
||||
@@ -367,7 +371,7 @@ describe("FindBar", () => {
|
||||
act(() => {
|
||||
button("pane-find-close").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
expect(controller.close).toHaveBeenCalledTimes(2);
|
||||
expect(container?.querySelector('[data-testid="pane-find-input"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("cleans up the active find adapter on pane deactivation and unmount", () => {
|
||||
@@ -436,8 +440,8 @@ describe("FindBar", () => {
|
||||
});
|
||||
|
||||
it("routes open find through the focused workspace pane without replacing pane focus", () => {
|
||||
const left = createController();
|
||||
const right = createController();
|
||||
const left = createController({ total: 7 });
|
||||
const right = createController({ total: 5 });
|
||||
const leftContent = buildWorkspacePaneContentModel({
|
||||
tab,
|
||||
paneId: "left",
|
||||
@@ -488,8 +492,8 @@ describe("FindBar", () => {
|
||||
});
|
||||
|
||||
changeInput("abc");
|
||||
expect(left.query).not.toHaveBeenCalled();
|
||||
expect(right.query).toHaveBeenCalledWith("abc");
|
||||
expect(container?.textContent).toContain("1 / 5");
|
||||
expect(container?.textContent).not.toContain("1 / 7");
|
||||
expect(focusLeft).not.toHaveBeenCalled();
|
||||
expect(focusRight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -45,11 +45,11 @@ function ensureOwnerFoundInPageListener(ownerContents: WebContents): void {
|
||||
export function registerPaseoBrowserWebContents(
|
||||
contents: WebContents,
|
||||
browserId: string,
|
||||
ownerContents?: WebContents,
|
||||
ownerContents: WebContents,
|
||||
): void {
|
||||
browserIdsByWebContentsId.set(contents.id, browserId);
|
||||
webContentsIdsByBrowserId.set(browserId, contents.id);
|
||||
if (ownerContents && !ownerContents.isDestroyed()) {
|
||||
if (!ownerContents.isDestroyed()) {
|
||||
ownerWebContentsIdsByBrowserId.set(browserId, ownerContents.id);
|
||||
ensureOwnerFoundInPageListener(ownerContents);
|
||||
}
|
||||
@@ -92,18 +92,17 @@ export function getPaseoBrowserWebContents(browserId: string): WebContents | nul
|
||||
return contents && !contents.isDestroyed() ? contents : null;
|
||||
}
|
||||
|
||||
export function setActivePaseoBrowserFind(browserId: string): boolean {
|
||||
export function setActivePaseoBrowserFind(browserId: string): void {
|
||||
const ownerContentsId = ownerWebContentsIdsByBrowserId.get(browserId);
|
||||
if (!ownerContentsId) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
const ownerContents = allWebContents.fromId(ownerContentsId);
|
||||
if (!ownerContents || ownerContents.isDestroyed()) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
ensureOwnerFoundInPageListener(ownerContents);
|
||||
activeFindBrowserIdsByOwnerWebContentsId.set(ownerContents.id, browserId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clearActivePaseoBrowserFind(browserId: string): void {
|
||||
|
||||
Reference in New Issue
Block a user