* feat(files): edit workspace files on web Keep source buffers synchronized with host file changes and require an explicit overwrite or reload when revisions diverge. * feat(panels): surface and protect modified tabs Expose tooltip and modification state through the generic panel boundary so tabs can show stable metadata and guard every close route consistently. * fix(tests): use portable fake timeout handle * fix(files): harden editor conflict handling Preserve modified panel state across tab eviction, use precise revisions for optimistic writes, coalesce concurrent file watchers, and localize the editor interface. * fix(files): close editor concurrency gaps Coalesce clean reloads, preserve subscriber identities and file permissions, suspend pending saves during close confirmation, and carry precise revisions through file reads. * test(files): expect read revision metadata
79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Image } from "react-native";
|
|
import { Globe } from "lucide-react-native";
|
|
import invariant from "tiny-invariant";
|
|
import { BrowserPane } from "@/components/browser-pane";
|
|
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
|
|
import type { PanelDescriptor, PanelIconProps, PanelRegistration } from "@/panels/panel-registry";
|
|
import { useBrowserStore } from "@/stores/browser-store";
|
|
import { useWorkspaceDirectory } from "@/stores/session-store-hooks";
|
|
|
|
function getBrowserLabel(input: { title: string; url: string }): string {
|
|
const title = input.title.trim();
|
|
if (title) {
|
|
return title;
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(input.url);
|
|
return parsed.hostname || input.url;
|
|
} catch {
|
|
return input.url;
|
|
}
|
|
}
|
|
|
|
function createBrowserTabIcon(faviconUrl: string | null) {
|
|
return function BrowserTabIcon({ size, color }: PanelIconProps) {
|
|
const source = useMemo(() => (faviconUrl ? { uri: faviconUrl } : undefined), []);
|
|
const imageStyle = useMemo(() => ({ width: size, height: size, borderRadius: 3 }), [size]);
|
|
|
|
if (faviconUrl) {
|
|
return <Image accessibilityIgnoresInvertColors source={source} style={imageStyle} />;
|
|
}
|
|
|
|
return <Globe size={size} color={color} />;
|
|
};
|
|
}
|
|
|
|
function useBrowserPanelDescriptor(target: {
|
|
kind: "browser";
|
|
browserId: string;
|
|
}): PanelDescriptor {
|
|
const browser = useBrowserStore((state) => state.browsersById[target.browserId] ?? null);
|
|
const url = browser?.url ?? "https://example.com";
|
|
const icon = createBrowserTabIcon(browser?.faviconUrl ?? null);
|
|
const label = getBrowserLabel({ title: browser?.title ?? "", url });
|
|
|
|
return {
|
|
label,
|
|
subtitle: url,
|
|
tooltip: url || label,
|
|
titleState: "ready",
|
|
icon,
|
|
statusBucket: browser?.isLoading ? "running" : null,
|
|
};
|
|
}
|
|
|
|
function BrowserPanel() {
|
|
const { serverId, workspaceId, target } = usePaneContext();
|
|
const { focusPane, isInteractive } = usePaneFocus();
|
|
const cwd = useWorkspaceDirectory(serverId, workspaceId);
|
|
invariant(target.kind === "browser", "BrowserPanel requires browser target");
|
|
return (
|
|
<BrowserPane
|
|
browserId={target.browserId}
|
|
serverId={serverId}
|
|
workspaceId={workspaceId}
|
|
cwd={cwd}
|
|
isInteractive={isInteractive}
|
|
onFocusPane={focusPane}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const browserPanelRegistration: PanelRegistration<"browser"> = {
|
|
kind: "browser",
|
|
component: BrowserPanel,
|
|
useDescriptor: useBrowserPanelDescriptor,
|
|
};
|