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", () => {
3333 command: "code",
3434 args: ["/tmp/workspace"],
3535 });
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 });
3645 }),
3746 );
3847
......@@ -64,6 +73,15 @@ describe("resolveEditorLaunch", () => {
6473 command: "code",
6574 args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
6675 });
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 });
6785 }),
6886 );
6987
apps/web/src/components/BranchToolbar.tsx+12-7
......@@ -1,6 +1,6 @@
11import type { GitBranch, ThreadId } from "@t3tools/contracts";
22import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3import { useOptimistic, useState } from "react";
3import { startTransition, useOptimistic, useState } from "react";
44
55import { newCommandId } from "../lib/utils";
66import {
......@@ -80,6 +80,11 @@ export default function BranchToolbar({
8080 canonicalActiveBranch,
8181 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,
8282 );
83 const setOptimisticBranchInTransition = (branch: string | null) => {
84 startTransition(() => {
85 setOptimisticBranch(branch);
86 });
87 };
8388 const branchNames = branches.map((branch) => branch.name);
8489 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
8590 const trimmedBranchQuery = branchQuery.trim();
......@@ -172,7 +177,7 @@ export default function BranchToolbar({
172177 const selectedBranchName = branch.isRemote
173178 ? deriveLocalBranchNameFromRemoteRef(branch.name)
174179 : branch.name;
175 setOptimisticBranch(selectedBranchName);
180 setOptimisticBranchInTransition(selectedBranchName);
176181
177182 checkoutMutation.mutate(branch.name, {
178183 onSuccess: async () => {
......@@ -184,13 +189,13 @@ export default function BranchToolbar({
184189 nextBranchName = status.branch;
185190 }
186191 }
187 setOptimisticBranch(nextBranchName);
192 setOptimisticBranchInTransition(nextBranchName);
188193 setThreadBranch(nextBranchName, activeWorktreePath);
189194 setIsBranchMenuOpen(false);
190195 onComposerFocusRequest?.();
191196 },
192197 onError: (error) => {
193 setOptimisticBranch(canonicalActiveBranch);
198 setOptimisticBranchInTransition(canonicalActiveBranch);
194199 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
195200 setIsBranchMenuOpen(true);
196201 },
......@@ -201,10 +206,10 @@ export default function BranchToolbar({
201206 const name = rawName.trim();
202207 const api = readNativeApi();
203208 if (!api || !activeThreadId || !branchCwd || !name || createBranchMutation.isPending) return;
204 setOptimisticBranch(name);
209 setOptimisticBranchInTransition(name);
205210 createBranchMutation.mutate(name, {
206211 onSuccess: () => {
207 setOptimisticBranch(name);
212 setOptimisticBranchInTransition(name);
208213 setThreadError(null);
209214 setThreadBranch(name, activeWorktreePath);
210215 setBranchQuery("");
......@@ -212,7 +217,7 @@ export default function BranchToolbar({
212217 onComposerFocusRequest?.();
213218 },
214219 onError: (error) => {
215 setOptimisticBranch(canonicalActiveBranch);
220 setOptimisticBranchInTransition(canonicalActiveBranch);
216221 setThreadError(error instanceof Error ? error.message : "Failed to create branch.");
217222 },
218223 });
apps/web/src/components/ChatView.tsx+6-1
......@@ -100,7 +100,7 @@ import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./u
100100import { Separator } from "./ui/separator";
101101import { Group, GroupSeparator } from "./ui/group";
102102import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu";
103import { CursorIcon, Icon, VisualStudioCode } from "./Icons";
103import { CursorIcon, Icon, VisualStudioCode, Zed } from "./Icons";
104104import { cn, isMacPlatform, isWindowsPlatform } from "~/lib/utils";
105105import { Badge } from "./ui/badge";
106106import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
......@@ -3693,6 +3693,11 @@ const OpenInPicker = memo(function OpenInPicker({
36933693 Icon: VisualStudioCode,
36943694 value: "vscode",
36953695 },
3696 {
3697 label: "Zed",
3698 Icon: Zed,
3699 value: "zed",
3700 },
36963701 {
36973702 label: isMacPlatform(navigator.platform)
36983703 ? "Finder"
apps/web/src/components/Icons.tsx+23
......@@ -119,3 +119,26 @@ export const VisualStudioCode: Icon = (props) => {
119119 </svg>
120120 );
121121};
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";
44export const EDITORS = [
55 { id: "cursor", label: "Cursor", command: "cursor" },
66 { id: "vscode", label: "VS Code", command: "code" },
7 { id: "zed", label: "Zed", command: "zed" },
78 { id: "file-manager", label: "File Manager", command: null },
89] as const;
910