Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import xcodeIcon from "@/assets/workspace-open-target-icons/xcode.png";
import { SplitButton, type SplitButtonAction } from "@/components/ui/split-button.js";

const WORKSPACE_OPEN_TARGET_ICONS: Record<WorkspaceOpenTargetId, string> = {
"default-app": finderIcon,
vscode: vscodeIcon,
cursor: cursorIcon,
"sublime-text": sublimeTextIcon,
Expand Down
66 changes: 66 additions & 0 deletions apps/app/src/hooks/useLocalOpenTargets.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,72 @@ describe("useLocalOpenTargets", () => {
});
});

it("opens editor requests in an editor target when the stored workspace target is Finder", async () => {
window.localStorage.setItem(WORKSPACE_OPEN_TARGET_STORAGE_KEY, "finder");
const state: LocalOpenTargetsFetchState = {
daemonStatus: {
connected: true,
hostId: "host-1",
protocolVersion: HOST_DAEMON_PROTOCOL_VERSION,
serverUrl: "http://localhost:3334",
supportsNativeFolderPicker: false,
platform: "darwin",
},
hostDaemonPort: 4123,
workspaceOpenTargets: [
{ id: "default-app", kind: "editor", label: "Default App" },
{ id: "finder", kind: "file-browser", label: "Finder" },
],
workspaceOpenTargetsStatus: 200,
};
const openTargetRequests: Array<
ReturnType<typeof openInTargetRequestSchema.parse>
> = [];
installLocalOpenTargetsFetchRoutes(state, openTargetRequests);
const modules = await importFreshLocalOpenTargetsModules();
const latestSnapshot: { current: LocalOpenTargetsSnapshot | null } = {
current: null,
};
await act(async () => {
render(
<LocalOpenTargetsCapture
modules={modules}
onSnapshot={(snapshot) => {
latestSnapshot.current = snapshot;
}}
/>,
{ wrapper: createSuspenseWrapper() },
);
});

await waitFor(() => {
const localOpenTargets = requireLocalOpenTargetsSnapshot(
latestSnapshot.current,
).localOpenTargets;
expect(localOpenTargets.preferredTarget?.label).toBe("Finder");
expect(localOpenTargets.preferredEditorTarget?.label).toBe("Default App");
});

await act(async () => {
await requireLocalOpenTargetsSnapshot(
latestSnapshot.current,
).localOpenTargets.openPathInPreferredEditorTarget({
lineNumber: 27,
path: "/tmp/workspace/file.md",
});
});

await waitFor(() => {
expect(openTargetRequests).toEqual([
{
lineNumber: 27,
path: "/tmp/workspace/file.md",
targetId: "default-app",
},
]);
});
});

it("stores an explicitly selected target for future opens", async () => {
const state: LocalOpenTargetsFetchState = {
daemonStatus: {
Expand Down
42 changes: 42 additions & 0 deletions apps/app/src/hooks/useLocalOpenTargets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ export interface OpenPathInTargetArgs extends OpenLocalPathRequest {
export interface OpenPathInPreferredTargetArgs extends OpenLocalPathRequest {}

export interface UseLocalOpenTargetsResult {
canOpenPreferredEditorTarget: boolean;
canOpenPreferredTarget: boolean;
openPathInPreferredEditorTarget: (
args: OpenPathInPreferredTargetArgs,
) => Promise<boolean>;
openPathInPreferredTarget: (
args: OpenPathInPreferredTargetArgs,
) => Promise<boolean>;
preferredEditorTarget: WorkspaceOpenTarget | null;
openPathInTarget: (args: OpenPathInTargetArgs) => Promise<boolean>;
preferredTarget: WorkspaceOpenTarget | null;
workspaceOpenTargets: WorkspaceOpenTarget[];
Expand Down Expand Up @@ -74,6 +79,16 @@ export function useLocalOpenTargets(
}),
[preferredTargetId, workspaceOpenTargets],
);
const preferredEditorTarget = useMemo(
() =>
resolvePreferredWorkspaceOpenTarget({
preferredTargetId,
targets: workspaceOpenTargets.filter(
(target) => target.kind === "editor",
),
}),
[preferredTargetId, workspaceOpenTargets],
);

const openPathInTarget = useCallback(
async (request: OpenPathInTargetArgs) => {
Expand Down Expand Up @@ -140,10 +155,37 @@ export function useLocalOpenTargets(
preferredTarget,
],
);
const openPathInPreferredEditorTarget = useCallback(
async (request: OpenPathInPreferredTargetArgs) => {
if (!preferredEditorTarget) {
appToast.error(LOCAL_OPEN_FAILURE_TITLE, {
description: getOpenUnavailableDescription({
hasDaemon,
}),
});
return false;
}

return openPathInTarget({
lineNumber: request.lineNumber,
path: request.path,
rememberTarget: false,
targetId: preferredEditorTarget.id,
});
},
[
hasDaemon,
openPathInTarget,
preferredEditorTarget,
],
);

return {
canOpenPreferredEditorTarget: preferredEditorTarget !== null,
canOpenPreferredTarget: preferredTarget !== null,
openPathInPreferredEditorTarget,
openPathInPreferredTarget,
preferredEditorTarget,
openPathInTarget,
preferredTarget,
workspaceOpenTargets,
Expand Down
21 changes: 21 additions & 0 deletions apps/app/src/lib/workspace-open-target-preference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ describe("resolvePreferredWorkspaceOpenTarget", () => {
kind: "editor",
label: "VS Code",
});
expect(
resolvePreferredWorkspaceOpenTarget({
preferredTargetId: null,
targets: [
{
id: "default-app",
kind: "editor",
label: "Default App",
},
{
id: "finder",
kind: "file-browser",
label: "Finder",
},
],
}),
).toEqual({
id: "default-app",
kind: "editor",
label: "Default App",
});
expect(
resolvePreferredWorkspaceOpenTarget({
preferredTargetId: null,
Expand Down
14 changes: 8 additions & 6 deletions apps/app/src/views/thread-detail/ThreadDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,9 @@ export function ThreadDetailView() {
threadEnvironmentIsLocal,
});
const {
canOpenPreferredEditorTarget,
canOpenPreferredTarget,
openPathInPreferredEditorTarget,
openPathInPreferredTarget,
openPathInTarget,
preferredTarget,
Expand Down Expand Up @@ -1025,18 +1027,18 @@ export function ThreadDetailView() {
: undefined;
const handleOpenFileInEditor = buildOpenInEditorHandler({
rootPath: localWorkspaceRootPath,
canOpenPreferredTarget,
openInPreferredTarget: openPathInPreferredTarget,
canOpenPreferredTarget: canOpenPreferredEditorTarget,
openInPreferredTarget: openPathInPreferredEditorTarget,
});
const handleOpenStorageFileInEditor = buildOpenInEditorHandler({
rootPath: threadEnvironmentIsLocal ? threadStorageRootPath : null,
canOpenPreferredTarget,
openInPreferredTarget: openPathInPreferredTarget,
canOpenPreferredTarget: canOpenPreferredEditorTarget,
openInPreferredTarget: openPathInPreferredEditorTarget,
});
const handleOpenHostFileInEditor =
threadEnvironmentIsLocal && canOpenPreferredTarget
threadEnvironmentIsLocal && canOpenPreferredEditorTarget
? (path: string) => {
void openPathInPreferredTarget({
void openPathInPreferredEditorTarget({
lineNumber: activeHostFileLineNumber,
path,
});
Expand Down
29 changes: 29 additions & 0 deletions apps/host-daemon/src/workspace-open-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe("workspace open targets", () => {

expect(targets.map((target) => target.id)).toEqual([
"zed",
"default-app",
"finder",
"terminal",
]);
Expand All @@ -111,6 +112,34 @@ describe("workspace open targets", () => {
).toBe(false);
});

it("opens paths with the macOS default app", async () => {
const workspacePath = await mkdtemp(path.join(tmpdir(), "bb-workspace-"));
const filePath = path.join(workspacePath, "notes.md");
const calls: ExecFileCall[] = [];
const execFile = createAvailableExecFile({ calls });

try {
await writeFile(filePath, "# Notes\n");

await openPathInTargetWithRuntime(
{
lineNumber: 12,
path: filePath,
targetId: "default-app",
},
createRuntime({ execFile }),
);

expect(calls.find((call) => call.file === "open")).toEqual({
file: "open",
args: ["--", filePath],
});
expect(calls.some((call) => call.file === "which")).toBe(false);
} finally {
await rm(workspacePath, { force: true, recursive: true });
}
});

it("falls back to application bundle paths when bundle id lookup misses", async () => {
const root = await mkdtemp(
path.join(tmpdir(), "bb-workspace-open-targets-"),
Expand Down
Loading