Skip to content
Merged
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
46 changes: 46 additions & 0 deletions apps/server/src/process/externalLauncher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,12 @@ for (const { platform, installPath, editor, args } of [
editor: "zed",
args: ["/workspace with spaces/file.ts:12:4"],
},
{
platform: "linux",
installPath: ".local/bin/px0",
editor: "px0",
args: ["/workspace with spaces/file.ts:12:4"],
},
] as const) {
it.effect.skipIf(windowsHost && platform !== "win32")(
`discovers and launches ${editor} outside PATH on ${platform}`,
Expand Down Expand Up @@ -1028,6 +1034,46 @@ for (const { platform, installPath, editor, args } of [
);
}

// px0 has no app bundle or Windows installer location: it relies on the PATH
// that server startup hydrates from the login shell (macOS) or the known CLI
// dirs such as %USERPROFILE%\.local\bin (Windows).
for (const { platform, binary } of [
{ platform: "darwin", binary: "px0" },
{ platform: "win32", binary: "px0.exe" },
] as const) {
it.effect.skipIf(windowsHost && platform !== "win32")(
`discovers and launches px0 from PATH on ${platform}`,
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const binDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-px0-" });
yield* fs.writeFileString(path.join(binDir, binary), "#!/bin/sh\n");
yield* fs.chmod(path.join(binDir, binary), 0o755);
let spawned: ChildProcess.StandardCommand | undefined;
yield* Effect.gen(function* () {
const launcher = yield* ExternalLauncher.ExternalLauncher;
assert.include(yield* launcher.resolveAvailableEditors(), "px0");
yield* launcher.launchEditor({ editor: "px0", cwd: "/workspace/file.ts:12:4" });
}).pipe(
Effect.provide(
testLayer({
platform,
env: { PATH: binDir, PATHEXT: ".COM;.EXE;.BAT;.CMD" },
onSpawn: (command) => {
spawned = command;
},
}),
),
);
assert.ok(spawned);
assert.equal(spawned.command, "px0");
assert.deepEqual(spawned.args, ["/workspace/file.ts:12:4"]);
assert.equal(spawned.options.shell, false);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
}

it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launchers first", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,14 @@ export const AntigravityIcon: Icon = (props) => (
</svg>
);

export const Px0Icon: Icon = (props) => (
<svg {...props} viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="5" fill="#0B0D12" />
<path d="M6 5h3v2H8v10h1v2H6zM18 5h-3v2h1v10h-1v2h3z" fill="#F2C14E" />
<rect x="10.5" y="7.75" width="3" height="8.5" rx="1.5" stroke="#F2C14E" strokeWidth="1.5" />
</svg>
);

export const OpenCodeIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#opencode__clip0_1311_94969)">
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/chat/OpenInPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
FinderIcon,
Icon,
KiroIcon,
Px0Icon,
TraeIcon,
VisualStudioCode,
VisualStudioCodeInsiders,
Expand Down Expand Up @@ -112,6 +113,11 @@ export const resolveOpenInOptions = (
value: "antigravity",
kind: "brand",
},
{
Icon: Px0Icon,
value: "px0",
kind: "brand",
},
{
Icon: IntelliJIdeaIcon,
value: "idea",
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export const EDITORS = [
remoteScheme: "zed",
},
{ id: "antigravity", label: "Antigravity", commands: ["agy"], launchStyle: "goto" },
// px0 serves its UI from a local server and opens it in the browser; each
// launch starts its own server on the next free port. It takes `path:line`.
{ id: "px0", label: "px0", commands: ["px0"], launchStyle: "direct-path" },
{ id: "idea", label: "IntelliJ IDEA", commands: ["idea"], launchStyle: "line-column" },
{ id: "aqua", label: "Aqua", commands: ["aqua"], launchStyle: "line-column" },
{ id: "clion", label: "CLion", commands: ["clion"], launchStyle: "line-column" },
Expand Down
Loading