mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
2 Commits
v0.1.51
...
add-websto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3876ffe61 | ||
|
|
8f136a1f92 |
BIN
packages/app/assets/images/editor-apps/webstorm.png
Normal file
BIN
packages/app/assets/images/editor-apps/webstorm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -1,5 +1,10 @@
|
||||
import { SquareTerminal } from "lucide-react-native";
|
||||
import { Image, type ImageSourcePropType } from "react-native";
|
||||
import type { EditorTargetId } from "@server/shared/messages";
|
||||
import {
|
||||
isKnownEditorTargetId,
|
||||
type EditorTargetId,
|
||||
type KnownEditorTargetId,
|
||||
} from "@server/shared/messages";
|
||||
|
||||
interface EditorAppIconProps {
|
||||
editorId: EditorTargetId;
|
||||
@@ -8,9 +13,10 @@ interface EditorAppIconProps {
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const EDITOR_APP_IMAGES: Record<EditorTargetId, ImageSourcePropType> = {
|
||||
const EDITOR_APP_IMAGES: Record<KnownEditorTargetId, ImageSourcePropType> = {
|
||||
cursor: require("../../../assets/images/editor-apps/cursor.png"),
|
||||
vscode: require("../../../assets/images/editor-apps/vscode.png"),
|
||||
webstorm: require("../../../assets/images/editor-apps/webstorm.png"),
|
||||
zed: require("../../../assets/images/editor-apps/zed.png"),
|
||||
finder: require("../../../assets/images/editor-apps/finder.png"),
|
||||
explorer: require("../../../assets/images/editor-apps/file-explorer.png"),
|
||||
@@ -18,10 +24,21 @@ const EDITOR_APP_IMAGES: Record<EditorTargetId, ImageSourcePropType> = {
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
export function hasBundledEditorAppIcon(
|
||||
editorId: EditorTargetId,
|
||||
): editorId is KnownEditorTargetId {
|
||||
return isKnownEditorTargetId(editorId);
|
||||
}
|
||||
|
||||
export function EditorAppIcon({
|
||||
editorId,
|
||||
size = 16,
|
||||
color,
|
||||
}: EditorAppIconProps) {
|
||||
if (!hasBundledEditorAppIcon(editorId)) {
|
||||
return <SquareTerminal size={size} color={color} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={EDITOR_APP_IMAGES[editorId]}
|
||||
|
||||
@@ -14,6 +14,12 @@ describe("resolvePreferredEditorId", () => {
|
||||
expect(resolvePreferredEditorId(["explorer", "vscode"], "finder")).toBe("explorer");
|
||||
});
|
||||
|
||||
it("keeps unknown editor ids when they are still available", () => {
|
||||
expect(resolvePreferredEditorId(["unknown-editor", "cursor"], "unknown-editor")).toBe(
|
||||
"unknown-editor",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null when no editors are available", () => {
|
||||
expect(resolvePreferredEditorId([], "cursor")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { listAvailableEditorTargets, openInEditorTarget } from "./editor-targets
|
||||
|
||||
describe("editor-targets", () => {
|
||||
it("lists available editors in deterministic order", async () => {
|
||||
const available = new Set(["code", "cursor", "explorer"]);
|
||||
const available = new Set(["code", "cursor", "explorer", "webstorm"]);
|
||||
|
||||
const editors = await listAvailableEditorTargets({
|
||||
platform: "win32",
|
||||
@@ -13,6 +13,7 @@ describe("editor-targets", () => {
|
||||
expect(editors).toEqual([
|
||||
{ id: "cursor", label: "Cursor" },
|
||||
{ id: "vscode", label: "VS Code" },
|
||||
{ id: "webstorm", label: "WebStorm" },
|
||||
{ id: "explorer", label: "Explorer" },
|
||||
]);
|
||||
});
|
||||
@@ -97,4 +98,19 @@ describe("editor-targets", () => {
|
||||
),
|
||||
).rejects.toThrow("Editor target unavailable: Finder");
|
||||
});
|
||||
|
||||
it("rejects unknown editor ids", async () => {
|
||||
await expect(
|
||||
openInEditorTarget(
|
||||
{
|
||||
editorId: "unknown-editor",
|
||||
path: "/tmp/repo",
|
||||
},
|
||||
{
|
||||
existsSync: () => true,
|
||||
findExecutable: () => null,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("Unknown editor target: unknown-editor");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { posix, win32 } from "node:path";
|
||||
import type { EditorTargetDescriptorPayload, EditorTargetId } from "../shared/messages.js";
|
||||
import type {
|
||||
EditorTargetDescriptorPayload,
|
||||
EditorTargetId,
|
||||
KnownEditorTargetId,
|
||||
} from "../shared/messages.js";
|
||||
import {
|
||||
findExecutable,
|
||||
quoteWindowsArgument,
|
||||
@@ -9,7 +13,7 @@ import {
|
||||
} from "../utils/executable.js";
|
||||
|
||||
type EditorTargetDefinition = {
|
||||
id: EditorTargetId;
|
||||
id: KnownEditorTargetId;
|
||||
label: string;
|
||||
command: string;
|
||||
platforms?: readonly NodeJS.Platform[];
|
||||
@@ -29,6 +33,7 @@ type OpenInEditorTargetDependencies = ListAvailableEditorTargetsDependencies & {
|
||||
const EDITOR_TARGETS: readonly EditorTargetDefinition[] = [
|
||||
{ id: "cursor", label: "Cursor", command: "cursor" },
|
||||
{ id: "vscode", label: "VS Code", command: "code" },
|
||||
{ id: "webstorm", label: "WebStorm", command: "webstorm" },
|
||||
{ id: "zed", label: "Zed", command: "zed" },
|
||||
{ id: "finder", label: "Finder", command: "open", platforms: ["darwin"] },
|
||||
{ id: "explorer", label: "Explorer", command: "explorer", platforms: ["win32"] },
|
||||
|
||||
@@ -8,6 +8,7 @@ import { homedir } from "node:os";
|
||||
import { z } from "zod";
|
||||
import type { ToolSet } from "ai";
|
||||
import {
|
||||
isLegacyEditorTargetId,
|
||||
serializeAgentStreamEvent,
|
||||
type AgentSnapshotPayload,
|
||||
type SessionInboundMessage,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
type SubscribeCheckoutDiffRequest,
|
||||
type UnsubscribeCheckoutDiffRequest,
|
||||
type DirectorySuggestionsRequest,
|
||||
type EditorTargetDescriptorPayload,
|
||||
type EditorTargetId,
|
||||
type ProjectPlacementPayload,
|
||||
type WorkspaceDescriptorPayload,
|
||||
@@ -202,13 +204,14 @@ const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
|
||||
// the entire session message if they encounter an unknown provider.
|
||||
const LEGACY_PROVIDER_IDS = new Set(["claude", "codex", "opencode"]);
|
||||
const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
|
||||
const MIN_VERSION_FLEXIBLE_EDITOR_IDS = "0.1.50";
|
||||
|
||||
function clientSupportsAllProviders(appVersion: string | null): boolean {
|
||||
function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean {
|
||||
if (!appVersion) return false;
|
||||
// Strip RC/prerelease suffix: "0.1.45-rc.4" → "0.1.45"
|
||||
const base = appVersion.replace(/-.*$/, "");
|
||||
const parts = base.split(".").map(Number);
|
||||
const minParts = MIN_VERSION_ALL_PROVIDERS.split(".").map(Number);
|
||||
const minParts = minVersion.split(".").map(Number);
|
||||
for (let i = 0; i < minParts.length; i++) {
|
||||
const a = parts[i] ?? 0;
|
||||
const b = minParts[i] ?? 0;
|
||||
@@ -218,6 +221,14 @@ function clientSupportsAllProviders(appVersion: string | null): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function clientSupportsAllProviders(appVersion: string | null): boolean {
|
||||
return isAppVersionAtLeast(appVersion, MIN_VERSION_ALL_PROVIDERS);
|
||||
}
|
||||
|
||||
function clientSupportsFlexibleEditorIds(appVersion: string | null): boolean {
|
||||
return isAppVersionAtLeast(appVersion, MIN_VERSION_FLEXIBLE_EDITOR_IDS);
|
||||
}
|
||||
|
||||
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
||||
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
|
||||
const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
|
||||
@@ -1215,6 +1226,15 @@ export class Session {
|
||||
return LEGACY_PROVIDER_IDS.has(provider);
|
||||
}
|
||||
|
||||
private filterEditorsForClient(
|
||||
editors: EditorTargetDescriptorPayload[],
|
||||
): EditorTargetDescriptorPayload[] {
|
||||
if (clientSupportsFlexibleEditorIds(this.appVersion)) {
|
||||
return editors;
|
||||
}
|
||||
return editors.filter((editor) => isLegacyEditorTargetId(editor.id));
|
||||
}
|
||||
|
||||
private matchesAgentFilter(options: {
|
||||
agent: AgentSnapshotPayload;
|
||||
project: ProjectPlacementPayload;
|
||||
@@ -6095,7 +6115,7 @@ export class Session {
|
||||
}
|
||||
|
||||
async getAvailableEditorTargets() {
|
||||
return listAvailableEditorTargets();
|
||||
return this.filterEditorsForClient(await listAvailableEditorTargets());
|
||||
}
|
||||
|
||||
async openEditorTarget(options: { editorId: EditorTargetId; path: string }): Promise<void> {
|
||||
@@ -7523,7 +7543,9 @@ export class Session {
|
||||
listStoredAgents: () => this.agentStorage.list(),
|
||||
listLiveAgents: () => this.agentManager.listAgents(),
|
||||
resolveAgentIdentifier: (identifier) => this.resolveAgentIdentifier(identifier),
|
||||
sendAgentMessage: (agentId, text) => this.handleSendAgentMessage(agentId, text),
|
||||
sendAgentMessage: async (agentId, text) => {
|
||||
await this.handleSendAgentMessage(agentId, text);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.emitChatRpcError(request, error);
|
||||
|
||||
@@ -61,7 +61,7 @@ function makeAgent(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionForWorkspaceTests(): Session {
|
||||
function createSessionForWorkspaceTests(options: { appVersion?: string | null } = {}): Session {
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
trace: vi.fn(),
|
||||
@@ -73,6 +73,7 @@ function createSessionForWorkspaceTests(): Session {
|
||||
|
||||
const session = new Session({
|
||||
clientId: "test-client",
|
||||
appVersion: options.appVersion ?? null,
|
||||
onMessage: vi.fn(),
|
||||
logger: logger as any,
|
||||
downloadTokenStore: {} as any,
|
||||
@@ -1246,19 +1247,52 @@ describe("workspace aggregation", () => {
|
||||
|
||||
test("list_available_editors_request returns available targets", async () => {
|
||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||
const session = createSessionForWorkspaceTests() as any;
|
||||
const session = createSessionForWorkspaceTests({ appVersion: "0.1.50" }) as any;
|
||||
|
||||
session.emit = (message: any) => emitted.push(message);
|
||||
session.getAvailableEditorTargets = async () => [
|
||||
{ id: "cursor", label: "Cursor" },
|
||||
{ id: "finder", label: "Finder" },
|
||||
];
|
||||
session.getAvailableEditorTargets = async () =>
|
||||
session.filterEditorsForClient([
|
||||
{ id: "cursor", label: "Cursor" },
|
||||
{ id: "webstorm", label: "WebStorm" },
|
||||
{ id: "finder", label: "Finder" },
|
||||
{ id: "unknown-editor", label: "Unknown Editor" },
|
||||
]);
|
||||
|
||||
await session.handleMessage({
|
||||
type: "list_available_editors_request",
|
||||
requestId: "req-editors",
|
||||
});
|
||||
|
||||
const response = emitted.find(
|
||||
(message) => message.type === "list_available_editors_response",
|
||||
) as any;
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.editors).toEqual([
|
||||
{ id: "cursor", label: "Cursor" },
|
||||
{ id: "webstorm", label: "WebStorm" },
|
||||
{ id: "finder", label: "Finder" },
|
||||
{ id: "unknown-editor", label: "Unknown Editor" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("list_available_editors_request filters unsupported ids for legacy clients", async () => {
|
||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||
const session = createSessionForWorkspaceTests({ appVersion: "0.1.49" }) as any;
|
||||
|
||||
session.emit = (message: any) => emitted.push(message);
|
||||
session.getAvailableEditorTargets = async () =>
|
||||
session.filterEditorsForClient([
|
||||
{ id: "cursor", label: "Cursor" },
|
||||
{ id: "webstorm", label: "WebStorm" },
|
||||
{ id: "unknown-editor", label: "Unknown Editor" },
|
||||
{ id: "finder", label: "Finder" },
|
||||
]);
|
||||
|
||||
await session.handleMessage({
|
||||
type: "list_available_editors_request",
|
||||
requestId: "req-editors-legacy",
|
||||
});
|
||||
|
||||
const response = emitted.find(
|
||||
(message) => message.type === "list_available_editors_response",
|
||||
) as any;
|
||||
|
||||
3
packages/server/src/shared/literal-union.ts
Normal file
3
packages/server/src/shared/literal-union.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Adapted from type-fest's LiteralUnion:
|
||||
// https://github.com/sindresorhus/type-fest/blob/main/source/literal-union.d.ts
|
||||
export type LiteralUnion<T extends U, U extends string> = T | (U & Record<never, never>);
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
LoopLogsResponseSchema,
|
||||
LoopStopResponseSchema,
|
||||
} from "../server/loop/rpc-schemas.js";
|
||||
import type { LiteralUnion } from "./literal-union.js";
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentModelDefinition,
|
||||
@@ -1096,14 +1097,32 @@ export const CreatePaseoWorktreeRequestSchema = z.object({
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const EditorTargetIdSchema = z.enum([
|
||||
// TODO(2026-07): Remove once most clients are on >=0.1.50 and support arbitrary editor ids.
|
||||
export const LEGACY_EDITOR_TARGET_IDS = [
|
||||
"cursor",
|
||||
"vscode",
|
||||
"zed",
|
||||
"finder",
|
||||
"explorer",
|
||||
"file-manager",
|
||||
]);
|
||||
] as const;
|
||||
|
||||
export const KNOWN_EDITOR_TARGET_IDS = [...LEGACY_EDITOR_TARGET_IDS, "webstorm"] as const;
|
||||
|
||||
export const KnownEditorTargetIdSchema = z.enum(KNOWN_EDITOR_TARGET_IDS);
|
||||
export const LegacyEditorTargetIdSchema = z.enum(LEGACY_EDITOR_TARGET_IDS);
|
||||
export const EditorTargetIdSchema = z.string().trim().min(1);
|
||||
|
||||
const KNOWN_EDITOR_TARGET_ID_SET = new Set<string>(KNOWN_EDITOR_TARGET_IDS);
|
||||
const LEGACY_EDITOR_TARGET_ID_SET = new Set<string>(LEGACY_EDITOR_TARGET_IDS);
|
||||
|
||||
export function isKnownEditorTargetId(value: string): value is KnownEditorTargetId {
|
||||
return KNOWN_EDITOR_TARGET_ID_SET.has(value);
|
||||
}
|
||||
|
||||
export function isLegacyEditorTargetId(value: string): value is LegacyEditorTargetId {
|
||||
return LEGACY_EDITOR_TARGET_ID_SET.has(value);
|
||||
}
|
||||
|
||||
export const EditorTargetDescriptorPayloadSchema = z.object({
|
||||
id: EditorTargetIdSchema,
|
||||
@@ -2626,7 +2645,9 @@ export type ProjectCheckoutLitePayload = z.infer<typeof ProjectCheckoutLitePaylo
|
||||
export type ProjectPlacementPayload = z.infer<typeof ProjectPlacementPayloadSchema>;
|
||||
export type WorkspaceStateBucket = z.infer<typeof WorkspaceStateBucketSchema>;
|
||||
export type WorkspaceDescriptorPayload = z.infer<typeof WorkspaceDescriptorPayloadSchema>;
|
||||
export type EditorTargetId = z.infer<typeof EditorTargetIdSchema>;
|
||||
export type KnownEditorTargetId = z.infer<typeof KnownEditorTargetIdSchema>;
|
||||
export type LegacyEditorTargetId = z.infer<typeof LegacyEditorTargetIdSchema>;
|
||||
export type EditorTargetId = LiteralUnion<KnownEditorTargetId, string>;
|
||||
export type EditorTargetDescriptorPayload = z.infer<typeof EditorTargetDescriptorPayloadSchema>;
|
||||
export type FetchAgentsResponseMessage = z.infer<typeof FetchAgentsResponseMessageSchema>;
|
||||
export type FetchWorkspacesResponseMessage = z.infer<typeof FetchWorkspacesResponseMessageSchema>;
|
||||
|
||||
@@ -38,6 +38,24 @@ describe("workspace message schemas", () => {
|
||||
expect(parsed.type).toBe("list_available_editors_request");
|
||||
});
|
||||
|
||||
test("parses open_in_editor_request with flexible editor ids", () => {
|
||||
const knownEditor = SessionInboundMessageSchema.parse({
|
||||
type: "open_in_editor_request",
|
||||
requestId: "req-open-webstorm",
|
||||
editorId: "webstorm",
|
||||
path: "/tmp/repo",
|
||||
});
|
||||
const unknownEditor = SessionInboundMessageSchema.parse({
|
||||
type: "open_in_editor_request",
|
||||
requestId: "req-open-custom",
|
||||
editorId: "unknown-editor",
|
||||
path: "/tmp/repo",
|
||||
});
|
||||
|
||||
expect(knownEditor.type).toBe("open_in_editor_request");
|
||||
expect(unknownEditor.type).toBe("open_in_editor_request");
|
||||
});
|
||||
|
||||
test("parses open_in_editor_response", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "open_in_editor_response",
|
||||
@@ -50,6 +68,33 @@ describe("workspace message schemas", () => {
|
||||
expect(parsed.type).toBe("open_in_editor_response");
|
||||
});
|
||||
|
||||
test("parses list_available_editors_response with unknown editor ids", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "list_available_editors_response",
|
||||
payload: {
|
||||
requestId: "req-editors",
|
||||
editors: [
|
||||
{ id: "cursor", label: "Cursor" },
|
||||
{ id: "unknown-editor", label: "Unknown Editor" },
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.type).toBe("list_available_editors_response");
|
||||
});
|
||||
|
||||
test("rejects empty editor ids", () => {
|
||||
const result = SessionInboundMessageSchema.safeParse({
|
||||
type: "open_in_editor_request",
|
||||
requestId: "req-open-empty",
|
||||
editorId: "",
|
||||
path: "/tmp/repo",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects invalid workspace update payload", () => {
|
||||
const result = SessionOutboundMessageSchema.safeParse({
|
||||
type: "workspace_update",
|
||||
|
||||
Reference in New Issue
Block a user