authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 16:43:23-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 18:53:21-08:00
log4a08686e749cca58dbab30307ab8445a3d5bef33
tree8f6435664af925f160934266a4361302f42a3ddb
parentb3de2bd926bf9e5f381d02cdcc32b85c39f7c4ed

Add Zed editor support across open-in flow

- add `zed` to shared editor contracts and Open In picker - add Zed icon and wire launch resolution tests on macOS - wrap branch optimistic updates in `startTransition` to avoid sync warnings

5 files changed, 60 insertions(+), 8 deletions(-)

apps/server/src/open.test.ts+18
...@@ -33,6 +33,15 @@ describe("resolveEditorLaunch", () => {...@@ -33,6 +33,15 @@ describe("resolveEditorLaunch", () => {
33 command: "code",33 command: "code",
34 args: ["/tmp/workspace"],34 args: ["/tmp/workspace"],
35 });35 });
36
37 const zedLaunch = yield* resolveEditorLaunch(
38 { cwd: "/tmp/workspace", editor: "zed" },
39 "darwin",
40 );
41 assert.deepEqual(zedLaunch, {
42 command: "zed",
43 args: ["/tmp/workspace"],
44 });
36 }),45 }),
37 );46 );
3847
...@@ -64,6 +73,15 @@ describe("resolveEditorLaunch", () => {...@@ -64,6 +73,15 @@ describe("resolveEditorLaunch", () => {
64 command: "code",73 command: "code",
65 args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],74 args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
66 });75 });
76
77 const zedLineAndColumn = yield* resolveEditorLaunch(
78 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" },
79 "darwin",
80 );
81 assert.deepEqual(zedLineAndColumn, {
82 command: "zed",
83 args: ["/tmp/workspace/src/open.ts:71:5"],
84 });
67 }),85 }),
68 );86 );
6987
apps/web/src/components/BranchToolbar.tsx+12-7
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { GitBranch, ThreadId } from "@t3tools/contracts";1import type { GitBranch, ThreadId } from "@t3tools/contracts";
2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3import { useOptimistic, useState } from "react";3import { startTransition, useOptimistic, useState } from "react";
44
5import { newCommandId } from "../lib/utils";5import { newCommandId } from "../lib/utils";
6import {6import {
...@@ -80,6 +80,11 @@ export default function BranchToolbar({...@@ -80,6 +80,11 @@ export default function BranchToolbar({
80 canonicalActiveBranch,80 canonicalActiveBranch,
81 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,81 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,
82 );82 );
83 const setOptimisticBranchInTransition = (branch: string | null) => {
84 startTransition(() => {
85 setOptimisticBranch(branch);
86 });
87 };
83 const branchNames = branches.map((branch) => branch.name);88 const branchNames = branches.map((branch) => branch.name);
84 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));89 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
85 const trimmedBranchQuery = branchQuery.trim();90 const trimmedBranchQuery = branchQuery.trim();
...@@ -172,7 +177,7 @@ export default function BranchToolbar({...@@ -172,7 +177,7 @@ export default function BranchToolbar({
172 const selectedBranchName = branch.isRemote177 const selectedBranchName = branch.isRemote
173 ? deriveLocalBranchNameFromRemoteRef(branch.name)178 ? deriveLocalBranchNameFromRemoteRef(branch.name)
174 : branch.name;179 : branch.name;
175 setOptimisticBranch(selectedBranchName);180 setOptimisticBranchInTransition(selectedBranchName);
176181
177 checkoutMutation.mutate(branch.name, {182 checkoutMutation.mutate(branch.name, {
178 onSuccess: async () => {183 onSuccess: async () => {
...@@ -184,13 +189,13 @@ export default function BranchToolbar({...@@ -184,13 +189,13 @@ export default function BranchToolbar({
184 nextBranchName = status.branch;189 nextBranchName = status.branch;
185 }190 }
186 }191 }
187 setOptimisticBranch(nextBranchName);192 setOptimisticBranchInTransition(nextBranchName);
188 setThreadBranch(nextBranchName, activeWorktreePath);193 setThreadBranch(nextBranchName, activeWorktreePath);
189 setIsBranchMenuOpen(false);194 setIsBranchMenuOpen(false);
190 onComposerFocusRequest?.();195 onComposerFocusRequest?.();
191 },196 },
192 onError: (error) => {197 onError: (error) => {
193 setOptimisticBranch(canonicalActiveBranch);198 setOptimisticBranchInTransition(canonicalActiveBranch);
194 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");199 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
195 setIsBranchMenuOpen(true);200 setIsBranchMenuOpen(true);
196 },201 },
...@@ -201,10 +206,10 @@ export default function BranchToolbar({...@@ -201,10 +206,10 @@ export default function BranchToolbar({
201 const name = rawName.trim();206 const name = rawName.trim();
202 const api = readNativeApi();207 const api = readNativeApi();
203 if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return;208 if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return;
204 setOptimisticBranch(name);209 setOptimisticBranchInTransition(name);
205 createBranchMutation.mutate(name, {210 createBranchMutation.mutate(name, {
206 onSuccess: () => {211 onSuccess: () => {
207 setOptimisticBranch(name);212 setOptimisticBranchInTransition(name);
208 setThreadError(null);213 setThreadError(null);
209 setThreadBranch(name, activeWorktreePath);214 setThreadBranch(name, activeWorktreePath);
210 setBranchQuery("");215 setBranchQuery("");
...@@ -212,7 +217,7 @@ export default function BranchToolbar({...@@ -212,7 +217,7 @@ export default function BranchToolbar({
212 onComposerFocusRequest?.();217 onComposerFocusRequest?.();
213 },218 },
214 onError: (error) => {219 onError: (error) => {
215 setOptimisticBranch(canonicalActiveBranch);220 setOptimisticBranchInTransition(canonicalActiveBranch);
216 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");221 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");
217 },222 },
218 });223 });
apps/web/src/components/ChatView.tsx+6-1
...@@ -100,7 +100,7 @@ import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./u...@@ -100,7 +100,7 @@ import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./u
100import { Separator } from "./ui/separator";100import { Separator } from "./ui/separator";
101import { Group, GroupSeparator } from "./ui/group";101import { Group, GroupSeparator } from "./ui/group";
102import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu";102import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu";
103import { CursorIcon, Icon, VisualStudioCode } from "./Icons";103import { CursorIcon, Icon, VisualStudioCode, Zed } from "./Icons";
104import { cn, isMacPlatform, isWindowsPlatform } from "~/lib/utils";104import { cn, isMacPlatform, isWindowsPlatform } from "~/lib/utils";
105import { Badge } from "./ui/badge";105import { Badge } from "./ui/badge";
106import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";106import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
...@@ -3693,6 +3693,11 @@ const OpenInPicker = memo(function OpenInPicker({...@@ -3693,6 +3693,11 @@ const OpenInPicker = memo(function OpenInPicker({
3693 Icon: VisualStudioCode,3693 Icon: VisualStudioCode,
3694 value: "vscode",3694 value: "vscode",
3695 },3695 },
3696 {
3697 label: "Zed",
3698 Icon: Zed,
3699 value: "zed",
3700 },
3696 {3701 {
3697 label: isMacPlatform(navigator.platform)3702 label: isMacPlatform(navigator.platform)
3698 ? "Finder"3703 ? "Finder"
apps/web/src/components/Icons.tsx+23
...@@ -119,3 +119,26 @@ export const VisualStudioCode: Icon = (props) => {...@@ -119,3 +119,26 @@ export const VisualStudioCode: Icon = (props) => {
119 </svg>119 </svg>
120 );120 );
121};121};
122
123export const Zed: Icon = (props) => {
124 const id = useId();
125 const clipPathId = `${id}-zed-logo-a`;
126
127 return (
128 <svg {...props} fill="none" viewBox="0 0 96 96">
129 <g clipPath={`url(#${clipPathId})`}>
130 <path
131 fill="currentColor"
132 fillRule="evenodd"
133 d="M9 6a3 3 0 0 0-3 3v66H0V9a9 9 0 0 1 9-9h80.379c4.009 0 6.016 4.847 3.182 7.682L43.055 57.187H57V51h6v7.688a4.5 4.5 0 0 1-4.5 4.5H37.055L26.743 73.5H73.5V36h6v37.5a6 6 0 0 1-6 6H20.743L10.243 90H87a3 3 0 0 0 3-3V21h6v66a9 9 0 0 1-9 9H6.621c-4.009 0-6.016-4.847-3.182-7.682L52.757 39H39v6h-6v-7.5a4.5 4.5 0 0 1 4.5-4.5h21.257l10.5-10.5H22.5V60h-6V22.5a6 6 0 0 1 6-6h52.757L85.757 6H9Z"
134 clipRule="evenodd"
135 />
136 </g>
137 <defs>
138 <clipPath id={clipPathId}>
139 <path fill="#fff" d="M0 0h96v96H0z" />
140 </clipPath>
141 </defs>
142 </svg>
143 );
144};
packages/contracts/src/editor.ts+1
...@@ -4,6 +4,7 @@ import { TrimmedNonEmptyString } from "./baseSchemas";...@@ -4,6 +4,7 @@ import { TrimmedNonEmptyString } from "./baseSchemas";
4export const EDITORS = [4export const EDITORS = [
5 { id: "cursor", label: "Cursor", command: "cursor" },5 { id: "cursor", label: "Cursor", command: "cursor" },
6 { id: "vscode", label: "VS Code", command: "code" },6 { id: "vscode", label: "VS Code", command: "code" },
7 { id: "zed", label: "Zed", command: "zed" },
7 { id: "file-manager", label: "File Manager", command: null },8 { id: "file-manager", label: "File Manager", command: null },
8] as const;9] as const;
910