дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(remote-dev): Remote Development in the project popup, opened in a new or this window
- The title bar project popup lists Remote Development… next to Open… and Clone Repository…. - Opening a remote project follows "Open project in" (new window / this window / ask) when this window already has a project, and confirms leaving the current one first. - A remote project already open in another window focuses that window. - A new window connects to the host itself and shows the connection progress on its welcome screen. - Recent remote projects get Open in New Window.
Цей коміт міститься в:
1 джерело
45c7f43c02
коміт
0222c84ace
15 files changed
+210
-62
No files matched your search
@@ -7,9 +7,10 @@ import type { Action } from "@/platform/actions";
|
||||
import { t } from "@/platform/i18n";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { basename, confirmLeaveProject, openProject, provideStartupLaunch, useProject } from "@/platform/project";
|
||||
import { connectRemote } from "@/platform/remote";
|
||||
import { holdSplash } from "@/platform/splash";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { ask } from "@/workbench/messages";
|
||||
import { chooseProjectWindow } from "@/workbench/projectWindow";
|
||||
|
||||
export const LAUNCH_EVENT = "openidea://launch";
|
||||
|
||||
@@ -58,42 +59,14 @@ export async function openProjectInWindow(path: string, newWindow: boolean): Pro
|
||||
}
|
||||
}
|
||||
|
||||
/** System Settings › Project › "Open project in": `new` | `same` | `ask`. */
|
||||
export function openPolicy(): "ask" | "new" | "same" {
|
||||
const v = useProject.getState().settings?.general?.openProjectIn;
|
||||
return v === "new" || v === "same" ? v : "ask";
|
||||
}
|
||||
|
||||
/** Opens `path` following "Open project in" when this window already has a project. */
|
||||
export async function openProjectWithPolicy(path: string): Promise<void> {
|
||||
if (desktopOnly()) return;
|
||||
const current = useProject.getState().project;
|
||||
if (!current) return openProjectInWindow(path, false);
|
||||
if (current.root === path) return;
|
||||
let mode = openPolicy();
|
||||
if (mode === "ask") {
|
||||
const r = await ask({
|
||||
title: t("app.window.askTitle"),
|
||||
body: t("app.window.askBody", { name: basename(path) }),
|
||||
buttons: [
|
||||
{ id: "new", label: t("app.window.askNew") },
|
||||
{ id: "same", label: t("app.window.askThis"), primary: true },
|
||||
{ id: "cancel", label: t("common.cancel") },
|
||||
],
|
||||
checkbox: t("app.window.askRemember"),
|
||||
});
|
||||
if (r.button !== "same" && r.button !== "new") return;
|
||||
mode = r.button;
|
||||
if (r.checked) {
|
||||
try {
|
||||
useProject.setState({
|
||||
settings: await call(commands.settingsSet("global", "general.openProjectIn", JSON.stringify(mode))),
|
||||
});
|
||||
} catch {
|
||||
/* the choice still applies to this open */
|
||||
}
|
||||
}
|
||||
}
|
||||
const mode = await chooseProjectWindow(basename(path));
|
||||
if (!mode) return;
|
||||
if (mode === "new") return openProjectInWindow(path, true);
|
||||
try {
|
||||
// A window that already has the project is focused instead.
|
||||
@@ -121,6 +94,17 @@ export async function consumeLaunch(): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
const launch = await commands.windowTakeLaunch().catch(() => null);
|
||||
if (launch?.project) await openHere(launch.project);
|
||||
else if (launch?.remote) {
|
||||
const { sshConfig, path } = launch.remote;
|
||||
await connectRemote(sshConfig, path).catch((e) =>
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
title: t("app.window.openFailed", { name: basename(path) }),
|
||||
body: errorText(e),
|
||||
key: `app.open:${path}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function windowActions(): Action[] {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/** Picks an SSH configuration and a folder on that host, then opens it in this window. */
|
||||
/** Picks an SSH configuration and a folder on that host, then opens it in this or a new window. */
|
||||
import { useState } from "react";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import { call, commands, type RemoteEntry } from "@/platform/ipc";
|
||||
import { connectRemote, useRemoteProgress } from "@/platform/remote";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { sshLabel } from "@/platform/remoteSettings";
|
||||
import { useWorkflowSettings } from "@/platform/workflowSettings";
|
||||
import { Button, Dialog, Icon, Input, ProgressBar, Select, Spinner } from "@/ui";
|
||||
import { openRemoteProject } from "@/workbench/projectWindow";
|
||||
import {
|
||||
browsableFolders,
|
||||
closeConnectDialog,
|
||||
@@ -52,8 +53,9 @@ function ConnectDialog() {
|
||||
setError(null);
|
||||
setBusy(true);
|
||||
try {
|
||||
await connectRemote(ssh, path.trim() || "~");
|
||||
closeConnectDialog();
|
||||
const target = path.trim() || "~";
|
||||
if (await openRemoteProject(ssh, target)) closeConnectDialog();
|
||||
else setBusy(false);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setBusy(false);
|
||||
@@ -62,6 +64,7 @@ function ConnectDialog() {
|
||||
|
||||
const options = configs.map((c) => ({ value: c.id, label: c.name.trim() || sshLabel(c), detail: sshLabel(c) }));
|
||||
const ready = !!ssh && !busy && !!path.trim();
|
||||
const connecting = useRemoteProgress((s) => s.progress !== null);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -70,7 +73,7 @@ function ConnectDialog() {
|
||||
onClose={closeConnectDialog}
|
||||
width={560}
|
||||
buttons={[
|
||||
{ label: busy ? "Connecting…" : "Open", role: "default", disabled: !ready, onClick: () => void connect() },
|
||||
{ label: connecting ? "Connecting…" : "Open", role: "default", disabled: !ready, onClick: () => void connect() },
|
||||
{ label: "Cancel", role: "cancel" },
|
||||
]}
|
||||
>
|
||||
@@ -83,7 +86,7 @@ function ConnectDialog() {
|
||||
>
|
||||
<p className={styles.hint}>
|
||||
{tx(
|
||||
"The project runs on the remote host; this window shows it. The IDE backend is installed there on first use.",
|
||||
"The project runs on the remote host; the IDE backend is installed there on first use.",
|
||||
)}
|
||||
</p>
|
||||
<label className={styles.label}>{tx("SSH configuration:")}</label>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function activate(): () => void {
|
||||
{
|
||||
id: "remote.connect",
|
||||
title: "Remote Development…",
|
||||
description: "Opens a project folder on an SSH host in this window; the IDE backend runs on that host.",
|
||||
description: "Opens a project folder on an SSH host; the IDE backend runs on that host.",
|
||||
category: "File",
|
||||
icon: "folder",
|
||||
keywords: ["ssh", "remote", "server", "gateway", "connect", "host"],
|
||||
|
||||
@@ -14,6 +14,7 @@ type Entry =
|
||||
const ACTIONS: { id: string; label: string; icon: IconName }[] = [
|
||||
{ id: "file.openProject", label: "Open…", icon: "folder" },
|
||||
{ id: "vcs.clone", label: "Clone Repository…", icon: "branch" },
|
||||
{ id: "remote.connect", label: "Remote Development…", icon: "terminal" },
|
||||
];
|
||||
|
||||
function useOpenProjects(): OpenProject[] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState, type KeyboardEvent, type MouseEvent, type ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import type { RecentProject } from "@/platform/ipc";
|
||||
import { call, commands, type RecentProject } from "@/platform/ipc";
|
||||
import { parseRemoteLocation } from "@/platform/remote";
|
||||
import {
|
||||
HighlightedText,
|
||||
@@ -39,8 +39,8 @@ export function ProjectBadge({ name, className }: { name: string; className?: st
|
||||
}
|
||||
|
||||
export function recentMenu(project: RecentProject, onOpened?: () => void): MenuItem[] {
|
||||
// A remote project opens in this window only, and has no local folder.
|
||||
const local = parseRemoteLocation(project.path) === null;
|
||||
// A remote project has no local folder.
|
||||
const remote = parseRemoteLocation(project.path);
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
id: "open",
|
||||
@@ -48,7 +48,17 @@ export function recentMenu(project: RecentProject, onOpened?: () => void): MenuI
|
||||
icon: "folder-open",
|
||||
run: () => void openRecent(project).then((ok) => ok && onOpened?.()),
|
||||
},
|
||||
{ kind: "action", action: "file.openInNewWindow", arg: project.path, label: "Open in New Window" },
|
||||
remote
|
||||
? {
|
||||
id: "openInNewWindow",
|
||||
label: "Open in New Window",
|
||||
icon: "folder-open",
|
||||
run: () =>
|
||||
void call(commands.windowOpenRemote(remote.sshConfig, remote.path, true))
|
||||
.then(() => onOpened?.())
|
||||
.catch(() => undefined),
|
||||
}
|
||||
: { kind: "action", action: "file.openInNewWindow", arg: project.path, label: "Open in New Window" },
|
||||
{
|
||||
id: "reveal",
|
||||
label: "Show in Finder",
|
||||
@@ -71,9 +81,7 @@ export function recentMenu(project: RecentProject, onOpened?: () => void): MenuI
|
||||
run: () => void askRemoveRecent(project),
|
||||
},
|
||||
];
|
||||
return local
|
||||
? items
|
||||
: items.filter((i) => !("id" in i && i.id === "reveal") && !("action" in i && i.action === "file.openInNewWindow"));
|
||||
return remote ? items.filter((i) => !("id" in i && i.id === "reveal")) : items;
|
||||
}
|
||||
|
||||
function relativeTime(ms: number | null): string | null {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useT, useTx } from "@/platform/i18n";
|
||||
import { isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemoteProgress } from "@/platform/remote";
|
||||
import { stageLabel } from "@/features/remote-dev/model";
|
||||
import { stageLabel, useConnectDialog } from "@/features/remote-dev/model";
|
||||
import { Icon, IconButton, Menu, ProgressBar, Spinner, type MenuItem, T } from "@/ui";
|
||||
import { useSettingsPages } from "@/workbench/contributions";
|
||||
import { ErrorBoundary, Slot } from "@/workbench/ErrorBoundary";
|
||||
@@ -113,7 +113,7 @@ function useFolderDrop(): boolean {
|
||||
return over;
|
||||
}
|
||||
|
||||
/** The last session's project opening at startup; a remote one shows how far connecting is. */
|
||||
/** A project opening at startup or in a new window; a remote one shows how far connecting is. */
|
||||
function Reopening({ location }: { location: string }) {
|
||||
const tx = useTx();
|
||||
const progress = useRemoteProgress((s) => s.progress);
|
||||
@@ -139,6 +139,10 @@ export function WelcomeView() {
|
||||
const t = useT();
|
||||
const version = useRecent((s) => s.version);
|
||||
const reopening = useSession((s) => s.reopening);
|
||||
// A remote project connecting with no dialog or list row to show it (a new window's).
|
||||
const connecting = useRemoteProgress((s) => s.location);
|
||||
const connectDialog = useConnectDialog((s) => s.open);
|
||||
const listOpening = useRecent((s) => s.opening);
|
||||
const opening = useProject((s) => s.opening);
|
||||
const Plugins = useSettingsPages((s) => s.pages.get(PLUGINS_PAGE)?.component);
|
||||
const [page, setPage] = useState<WelcomePage>("projects");
|
||||
@@ -150,6 +154,7 @@ export function WelcomeView() {
|
||||
}, []);
|
||||
|
||||
if (reopening) return <Reopening location={reopening} />;
|
||||
if (connecting && !connectDialog && !listOpening) return <Reopening location={connecting} />;
|
||||
|
||||
return (
|
||||
<div className={styles.welcome}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { connectRemote, forgetRemote, parseRemoteLocation, recentRemotes, remote
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { fuzzyMatch } from "@/ui";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { openRemoteProject } from "@/workbench/projectWindow";
|
||||
|
||||
interface RecentState {
|
||||
projects: RecentProject[];
|
||||
@@ -131,10 +132,11 @@ async function openRemote(
|
||||
project: Pick<RecentProject, "path" | "name">,
|
||||
remote: { sshConfig: string; path: string },
|
||||
): Promise<boolean> {
|
||||
useRecent.setState({ opening: project.path });
|
||||
try {
|
||||
await connectRemote(remote.sshConfig, remote.path);
|
||||
return true;
|
||||
return await openRemoteProject(remote.sshConfig, remote.path, async () => {
|
||||
useRecent.setState({ opening: project.path });
|
||||
await connectRemote(remote.sshConfig, remote.path);
|
||||
});
|
||||
} catch (e) {
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
|
||||
Reference in new issue
Заблокувати користувача