| author | |
| committer | |
| log | c643de8ba70beab1f49d9176e8bb745cac807ed5 |
| tree | 5d2105b39fce000ff1f7dde69fed83e0f58e5e05 |
| parent | 9accd1ffb758431dd80b8c1b79ceb5dc618abaca |
- add desktop IPC handler to validate and open http(s) URLs via Electron shell
- expose `shell.openExternal` in shared NativeApi contracts and web bridge
- route Git PR opening and native API reads through the new `useNativeApi` hook
Co-authored-by: codex <codex@users.noreply.github.com>11 files changed, 80 insertions(+), 36 deletions(-)
apps/desktop/src/main.ts+27-1| ... | @@ -5,7 +5,7 @@ import net from "node:net"; | ... | @@ -5,7 +5,7 @@ import net from "node:net"; |
| 5 | import os from "node:os"; | 5 | import os from "node:os"; |
| 6 | import path from "node:path"; | 6 | import path from "node:path"; |
| 7 | 7 | ||
| 8 | import { app, BrowserWindow, dialog, ipcMain, Menu } from "electron"; | 8 | import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from "electron"; |
| 9 | 9 | ||
| 10 | import { fixPath } from "./fixPath"; | 10 | import { fixPath } from "./fixPath"; |
| 11 | 11 | ||
| ... | @@ -13,6 +13,7 @@ fixPath(); | ... | @@ -13,6 +13,7 @@ fixPath(); |
| 13 | 13 | ||
| 14 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; | 14 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; |
| 15 | const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; | 15 | const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; |
| 16 | const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; | ||
| 16 | const ROOT_DIR = path.resolve(__dirname, "../../.."); | 17 | const ROOT_DIR = path.resolve(__dirname, "../../.."); |
| 17 | const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.mjs"); | 18 | const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.mjs"); |
| 18 | const WEB_ENTRY = path.join(ROOT_DIR, "apps/web/dist/index.html"); | 19 | const WEB_ENTRY = path.join(ROOT_DIR, "apps/web/dist/index.html"); |
| ... | @@ -159,6 +160,31 @@ function registerIpcHandlers(): void { | ... | @@ -159,6 +160,31 @@ function registerIpcHandlers(): void { |
| 159 | }); | 160 | }); |
| 160 | }); | 161 | }); |
| 161 | }); | 162 | }); |
| 163 | |||
| 164 | ipcMain.removeHandler(OPEN_EXTERNAL_CHANNEL); | ||
| 165 | ipcMain.handle(OPEN_EXTERNAL_CHANNEL, async (_event, rawUrl: unknown) => { | ||
| 166 | if (typeof rawUrl !== "string" || rawUrl.length === 0) { | ||
| 167 | return false; | ||
| 168 | } | ||
| 169 | |||
| 170 | let parsedUrl: URL; | ||
| 171 | try { | ||
| 172 | parsedUrl = new URL(rawUrl); | ||
| 173 | } catch { | ||
| 174 | return false; | ||
| 175 | } | ||
| 176 | |||
| 177 | if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { | ||
| 178 | return false; | ||
| 179 | } | ||
| 180 | |||
| 181 | try { | ||
| 182 | await shell.openExternal(parsedUrl.toString()); | ||
| 183 | return true; | ||
| 184 | } catch { | ||
| 185 | return false; | ||
| 186 | } | ||
| 187 | }); | ||
| 162 | } | 188 | } |
| 163 | 189 | ||
| 164 | function createWindow(): BrowserWindow { | 190 | function createWindow(): BrowserWindow { |
apps/desktop/src/preload.ts+2| ... | @@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from "electron"; | ... | @@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from "electron"; |
| 2 | 2 | ||
| 3 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; | 3 | const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; |
| 4 | const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; | 4 | const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; |
| 5 | const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; | ||
| 5 | const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null; | 6 | const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null; |
| 6 | 7 | ||
| 7 | contextBridge.exposeInMainWorld("desktopBridge", { | 8 | contextBridge.exposeInMainWorld("desktopBridge", { |
| ... | @@ -9,4 +10,5 @@ contextBridge.exposeInMainWorld("desktopBridge", { | ... | @@ -9,4 +10,5 @@ contextBridge.exposeInMainWorld("desktopBridge", { |
| 9 | pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL) as Promise<string | null>, | 10 | pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL) as Promise<string | null>, |
| 10 | showContextMenu: (items: { id: string; label: string }[]) => | 11 | showContextMenu: (items: { id: string; label: string }[]) => |
| 11 | ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items) as Promise<string | null>, | 12 | ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items) as Promise<string | null>, |
| 13 | openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url) as Promise<boolean>, | ||
| 12 | }); | 14 | }); |
apps/web/src/App.tsx+5-5| ... | @@ -1,18 +1,18 @@ | ... | @@ -1,18 +1,18 @@ |
| 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
| 2 | import { useEffect, useMemo, useRef } from "react"; | 2 | import { useEffect, useRef } from "react"; |
| 3 | 3 | ||
| 4 | import ChatView from "./components/ChatView"; | 4 | import ChatView from "./components/ChatView"; |
| 5 | import DiffPanel from "./components/DiffPanel"; | 5 | import DiffPanel from "./components/DiffPanel"; |
| 6 | import Sidebar from "./components/Sidebar"; | 6 | import Sidebar from "./components/Sidebar"; |
| 7 | import { isElectron } from "./env"; | 7 | import { isElectron } from "./env"; |
| 8 | import { DEFAULT_MODEL } from "./model-logic"; | 8 | import { DEFAULT_MODEL } from "./model-logic"; |
| 9 | import { readNativeApi } from "./session-logic"; | ||
| 10 | import { StoreProvider, useStore } from "./store"; | 9 | import { StoreProvider, useStore } from "./store"; |
| 11 | import { DEFAULT_THREAD_TERMINAL_HEIGHT } from "./types"; | 10 | import { DEFAULT_THREAD_TERMINAL_HEIGHT } from "./types"; |
| 12 | import { onServerWelcome } from "./wsNativeApi"; | 11 | import { onServerWelcome } from "./wsNativeApi"; |
| 12 | import { useNativeApi } from "./hooks/useNativeApi"; | ||
| 13 | 13 | ||
| 14 | function EventRouter() { | 14 | function EventRouter() { |
| 15 | const api = useMemo(() => readNativeApi(), []); | 15 | const api = useNativeApi(); |
| 16 | const { dispatch } = useStore(); | 16 | const { dispatch } = useStore(); |
| 17 | const activeAssistantItemRef = useRef<string | null>(null); | 17 | const activeAssistantItemRef = useRef<string | null>(null); |
| 18 | 18 | ||
| ... | @@ -109,7 +109,7 @@ function AutoProjectBootstrap() { | ... | @@ -109,7 +109,7 @@ function AutoProjectBootstrap() { |
| 109 | } | 109 | } |
| 110 | 110 | ||
| 111 | function DesktopProjectBootstrap() { | 111 | function DesktopProjectBootstrap() { |
| 112 | const api = useMemo(() => readNativeApi(), []); | 112 | const api = useNativeApi(); |
| 113 | const { dispatch } = useStore(); | 113 | const { dispatch } = useStore(); |
| 114 | const bootstrappedRef = useRef(false); | 114 | const bootstrappedRef = useRef(false); |
| 115 | 115 | ||
| ... | @@ -159,7 +159,7 @@ function DesktopProjectBootstrap() { | ... | @@ -159,7 +159,7 @@ function DesktopProjectBootstrap() { |
| 159 | } | 159 | } |
| 160 | 160 | ||
| 161 | function Layout() { | 161 | function Layout() { |
| 162 | const api = useMemo(() => readNativeApi(), []); | 162 | const api = useNativeApi(); |
| 163 | const { state } = useStore(); | 163 | const { state } = useStore(); |
| 164 | 164 | ||
| 165 | if (!api) { | 165 | if (!api) { |
apps/web/src/components/BranchToolbar.tsx+8-4| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | import type { GitBranch } from "@t3tools/contracts"; | 1 | import type { GitBranch } from "@t3tools/contracts"; |
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 3 | import { useEffect, useMemo, useRef, useState } from "react"; | 3 | import { useEffect, useRef, useState } from "react"; |
| 4 | 4 | ||
| 5 | import { readNativeApi } from "../session-logic"; | ||
| 6 | import { useStore } from "../store"; | 5 | import { useStore } from "../store"; |
| 6 | import { useNativeApi } from "../hooks/useNativeApi"; | ||
| 7 | 7 | ||
| 8 | interface BranchToolbarProps { | 8 | interface BranchToolbarProps { |
| 9 | envMode: "local" | "worktree"; | 9 | envMode: "local" | "worktree"; |
| ... | @@ -13,7 +13,7 @@ interface BranchToolbarProps { | ... | @@ -13,7 +13,7 @@ interface BranchToolbarProps { |
| 13 | 13 | ||
| 14 | export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { | 14 | export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: BranchToolbarProps) { |
| 15 | const { state, dispatch } = useStore(); | 15 | const { state, dispatch } = useStore(); |
| 16 | const api = useMemo(() => readNativeApi(), []); | 16 | const api = useNativeApi(); |
| 17 | const queryClient = useQueryClient(); | 17 | const queryClient = useQueryClient(); |
| 18 | 18 | ||
| 19 | const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); | 19 | const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); |
| ... | @@ -254,7 +254,11 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B | ... | @@ -254,7 +254,11 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 254 | <span className="truncate">{branch.name}</span> | 254 | <span className="truncate">{branch.name}</span> |
| 255 | {(branch.current || branch.isDefault || hasSecondaryWorktree) && ( | 255 | {(branch.current || branch.isDefault || hasSecondaryWorktree) && ( |
| 256 | <span className="shrink-0 text-[10px] text-muted-foreground/45"> | 256 | <span className="shrink-0 text-[10px] text-muted-foreground/45"> |
| 257 | {branch.current ? "current" : hasSecondaryWorktree ? "worktree" : "default"} | 257 | {branch.current |
| 258 | ? "current" | ||
| 259 | : hasSecondaryWorktree | ||
| 260 | ? "worktree" | ||
| 261 | : "default"} | ||
| 258 | </span> | 262 | </span> |
| 259 | )} | 263 | )} |
| 260 | </button> | 264 | </button> |
apps/web/src/components/ChatView.tsx+3-5| ... | @@ -31,7 +31,6 @@ import { | ... | @@ -31,7 +31,6 @@ import { |
| 31 | formatDuration, | 31 | formatDuration, |
| 32 | formatElapsed, | 32 | formatElapsed, |
| 33 | formatTimestamp, | 33 | formatTimestamp, |
| 34 | readNativeApi, | ||
| 35 | } from "../session-logic"; | 34 | } from "../session-logic"; |
| 36 | import { useStore } from "../store"; | 35 | import { useStore } from "../store"; |
| 37 | import BranchToolbar from "./BranchToolbar"; | 36 | import BranchToolbar from "./BranchToolbar"; |
| ... | @@ -39,6 +38,7 @@ import GitActionsControl from "./GitActionsControl"; | ... | @@ -39,6 +38,7 @@ import GitActionsControl from "./GitActionsControl"; |
| 39 | import { isTerminalToggleShortcut } from "../terminal-shortcuts"; | 38 | import { isTerminalToggleShortcut } from "../terminal-shortcuts"; |
| 40 | import ChatMarkdown from "./ChatMarkdown"; | 39 | import ChatMarkdown from "./ChatMarkdown"; |
| 41 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; | 40 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 41 | import { useNativeApi } from "../hooks/useNativeApi"; | ||
| 42 | 42 | ||
| 43 | function formatMessageMeta(createdAt: string, duration: string | null): string { | 43 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| 44 | if (!duration) return formatTimestamp(createdAt); | 44 | if (!duration) return formatTimestamp(createdAt); |
| ... | @@ -133,7 +133,7 @@ function derivePendingApprovals(events: ProviderEvent[]): PendingApprovalCard[] | ... | @@ -133,7 +133,7 @@ function derivePendingApprovals(events: ProviderEvent[]): PendingApprovalCard[] |
| 133 | 133 | ||
| 134 | export default function ChatView() { | 134 | export default function ChatView() { |
| 135 | const { state, dispatch } = useStore(); | 135 | const { state, dispatch } = useStore(); |
| 136 | const api = useMemo(() => readNativeApi(), []); | 136 | const api = useNativeApi(); |
| 137 | const [prompt, setPrompt] = useState(""); | 137 | const [prompt, setPrompt] = useState(""); |
| 138 | const [isSending, setIsSending] = useState(false); | 138 | const [isSending, setIsSending] = useState(false); |
| 139 | const [isConnecting, setIsConnecting] = useState(false); | 139 | const [isConnecting, setIsConnecting] = useState(false); |
| ... | @@ -694,9 +694,7 @@ export default function ChatView() { | ... | @@ -694,9 +694,7 @@ export default function ChatView() { |
| 694 | </div> | 694 | </div> |
| 695 | )} | 695 | )} |
| 696 | {/* Git actions */} | 696 | {/* Git actions */} |
| 697 | {activeProject && ( | 697 | {activeProject && <GitActionsControl api={api} gitCwd={gitCwd} />} |
| 698 | <GitActionsControl api={api} gitCwd={gitCwd} /> | ||
| 699 | )} | ||
| 700 | {/* Diff toggle */} | 698 | {/* Diff toggle */} |
| 701 | <button | 699 | <button |
| 702 | type="button" | 700 | type="button" |
apps/web/src/components/GitActionsControl.tsx+10-17| ... | @@ -151,18 +151,6 @@ function buildGitActionMenuItems( | ... | @@ -151,18 +151,6 @@ function buildGitActionMenuItems( |
| 151 | gitStatus.hasUpstream && | 151 | gitStatus.hasUpstream && |
| 152 | gitStatus.behindCount === 0; | 152 | gitStatus.behindCount === 0; |
| 153 | 153 | ||
| 154 | if (!hasChanges && !hasAhead && hasOpenPr) { | ||
| 155 | return [ | ||
| 156 | { | ||
| 157 | id: "pr", | ||
| 158 | label: "Open PR", | ||
| 159 | disabled: !canOpenPr, | ||
| 160 | icon: "pr", | ||
| 161 | kind: "open_pr", | ||
| 162 | }, | ||
| 163 | ]; | ||
| 164 | } | ||
| 165 | |||
| 166 | return [ | 154 | return [ |
| 167 | { | 155 | { |
| 168 | id: "commit", | 156 | id: "commit", |
| ... | @@ -344,6 +332,10 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -344,6 +332,10 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 344 | const openExistingPr = useCallback(() => { | 332 | const openExistingPr = useCallback(() => { |
| 345 | setIsGitMenuOpen(false); | 333 | setIsGitMenuOpen(false); |
| 346 | setGitActionError(null); | 334 | setGitActionError(null); |
| 335 | if (!api) { | ||
| 336 | setGitActionError("Link opening is unavailable."); | ||
| 337 | return; | ||
| 338 | } | ||
| 347 | 339 | ||
| 348 | const prUrl = gitStatus?.openPr?.url ?? null; | 340 | const prUrl = gitStatus?.openPr?.url ?? null; |
| 349 | if (!prUrl) { | 341 | if (!prUrl) { |
| ... | @@ -351,11 +343,12 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -351,11 +343,12 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 351 | return; | 343 | return; |
| 352 | } | 344 | } |
| 353 | 345 | ||
| 354 | const popup = window.open(prUrl, "_blank", "noopener,noreferrer"); | 346 | void api.shell.openExternal(prUrl).catch((error) => { |
| 355 | if (!popup) { | 347 | setGitActionError( |
| 356 | setGitActionError("Unable to open PR. Allow popups and try again."); | 348 | error instanceof Error ? error.message : "Unable to open PR link.", |
| 357 | } | 349 | ); |
| 358 | }, [gitStatus?.openPr?.url]); | 350 | }); |
| 351 | }, [api, gitStatus?.openPr?.url]); | ||
| 359 | 352 | ||
| 360 | const runGitAction = useCallback(async () => { | 353 | const runGitAction = useCallback(async () => { |
| 361 | if (!api || !gitCwd || !isGitModalOpen) return; | 354 | if (!api || !gitCwd || !isGitModalOpen) return; |
apps/web/src/components/Sidebar.tsx+3-3| ... | @@ -1,11 +1,11 @@ | ... | @@ -1,11 +1,11 @@ |
| 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; | 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; |
| 2 | import { useCallback, useEffect, useMemo, useState } from "react"; | 2 | import { useCallback, useEffect, useState } from "react"; |
| 3 | import { isElectron } from "../env"; | 3 | import { isElectron } from "../env"; |
| 4 | import { useTheme } from "../hooks/useTheme"; | 4 | import { useTheme } from "../hooks/useTheme"; |
| 5 | import { DEFAULT_MODEL } from "../model-logic"; | 5 | import { DEFAULT_MODEL } from "../model-logic"; |
| 6 | import { readNativeApi } from "../session-logic"; | ||
| 7 | import { useStore } from "../store"; | 6 | import { useStore } from "../store"; |
| 8 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, type Project } from "../types"; | 7 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, type Project } from "../types"; |
| 8 | import { useNativeApi } from "../hooks/useNativeApi"; | ||
| 9 | 9 | ||
| 10 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; | 10 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; |
| 11 | function formatRelativeTime(iso: string): string { | 11 | function formatRelativeTime(iso: string): string { |
| ... | @@ -35,7 +35,7 @@ function threadStatusLabel( | ... | @@ -35,7 +35,7 @@ function threadStatusLabel( |
| 35 | 35 | ||
| 36 | export default function Sidebar() { | 36 | export default function Sidebar() { |
| 37 | const { state, dispatch } = useStore(); | 37 | const { state, dispatch } = useStore(); |
| 38 | const api = useMemo(() => readNativeApi(), []); | 38 | const api = useNativeApi(); |
| 39 | const { theme, setTheme } = useTheme(); | 39 | const { theme, setTheme } = useTheme(); |
| 40 | const [addingProject, setAddingProject] = useState(false); | 40 | const [addingProject, setAddingProject] = useState(false); |
| 41 | const [newCwd, setNewCwd] = useState(""); | 41 | const [newCwd, setNewCwd] = useState(""); |
apps/web/src/hooks/useNativeApi.ts created+6| ... | @@ -0,0 +1,6 @@ | ||
| 1 | import { useMemo } from "react"; | ||
| 2 | import { readNativeApi } from "../session-logic"; | ||
| 3 | |||
| 4 | export function useNativeApi() { | ||
| 5 | return useMemo(() => readNativeApi(), []); | ||
| 6 | } | ||
apps/web/src/vite-env.d.ts+1| ... | @@ -6,6 +6,7 @@ interface DesktopBridge { | ... | @@ -6,6 +6,7 @@ interface DesktopBridge { |
| 6 | getWsUrl: () => string | null; | 6 | getWsUrl: () => string | null; |
| 7 | pickFolder: () => Promise<string | null>; | 7 | pickFolder: () => Promise<string | null>; |
| 8 | showContextMenu: (items: readonly { id: string; label: string }[]) => Promise<string | null>; | 8 | showContextMenu: (items: readonly { id: string; label: string }[]) => Promise<string | null>; |
| 9 | openExternal: (url: string) => Promise<boolean>; | ||
| 9 | } | 10 | } |
| 10 | 11 | ||
| 11 | declare global { | 12 | declare global { |
apps/web/src/wsNativeApi.ts+14| ... | @@ -96,6 +96,20 @@ export function createWsNativeApi(): NativeApi { | ... | @@ -96,6 +96,20 @@ export function createWsNativeApi(): NativeApi { |
| 96 | shell: { | 96 | shell: { |
| 97 | openInEditor: (cwd, editor) => | 97 | openInEditor: (cwd, editor) => |
| 98 | transport.request(WS_METHODS.shellOpenInEditor, { cwd, editor }), | 98 | transport.request(WS_METHODS.shellOpenInEditor, { cwd, editor }), |
| 99 | openExternal: async (url) => { | ||
| 100 | if (window.desktopBridge) { | ||
| 101 | const opened = await window.desktopBridge.openExternal(url); | ||
| 102 | if (!opened) { | ||
| 103 | throw new Error("Unable to open link."); | ||
| 104 | } | ||
| 105 | return; | ||
| 106 | } | ||
| 107 | |||
| 108 | const popup = window.open(url, "_blank", "noopener,noreferrer"); | ||
| 109 | if (!popup) { | ||
| 110 | throw new Error("Unable to open link. Allow popups and try again."); | ||
| 111 | } | ||
| 112 | }, | ||
| 99 | }, | 113 | }, |
| 100 | git: { | 114 | git: { |
| 101 | status: (input) => transport.request(WS_METHODS.gitStatus, input), | 115 | status: (input) => transport.request(WS_METHODS.gitStatus, input), |
packages/contracts/src/ipc.ts+1-1| ... | @@ -89,6 +89,7 @@ export interface NativeApi { | ... | @@ -89,6 +89,7 @@ export interface NativeApi { |
| 89 | }; | 89 | }; |
| 90 | shell: { | 90 | shell: { |
| 91 | openInEditor: (cwd: string, editor: EditorId) => Promise<void>; | 91 | openInEditor: (cwd: string, editor: EditorId) => Promise<void>; |
| 92 | openExternal: (url: string) => Promise<void>; | ||
| 92 | }; | 93 | }; |
| 93 | git: { | 94 | git: { |
| 94 | // Existing branch/worktree API | 95 | // Existing branch/worktree API |
| ... | @@ -111,4 +112,3 @@ export interface NativeApi { | ... | @@ -111,4 +112,3 @@ export interface NativeApi { |
| 111 | ) => Promise<T | null>; | 112 | ) => Promise<T | null>; |
| 112 | }; | 113 | }; |
| 113 | } | 114 | } |
| 114 |