From e8ce510f72a73255d8e5881b398911c1c64030b6 Mon Sep 17 00:00:00 2001 From: estib Date: Mon, 11 Aug 2025 18:48:22 +0200 Subject: [PATCH 1/3] Refactor backend, unify interface, remove deprecated code Refactor backend to separate tauri and web implementations, add unified backend interface, and remove deprecated code. - Added a new backend interface (IBackend) with implementations for Tauri and Web. - Refactored tauri.ts to use new backend abstraction and exported new tauri functions. - Added web backend (web.ts) implementation for web builds. - Cleaned up ipc.ts and removed deprecated code. - Removed deprecated backendService.svelte.ts stub. --- .../src/lib/backend/backendService.svelte.ts | 1 - apps/desktop/src/lib/backend/index.ts | 84 +++++++++++ apps/desktop/src/lib/backend/ipc.ts | 116 -------------- apps/desktop/src/lib/backend/tauri.ts | 50 ++++-- apps/desktop/src/lib/backend/web.ts | 142 ++++++++++++++++++ 5 files changed, 267 insertions(+), 126 deletions(-) delete mode 100644 apps/desktop/src/lib/backend/backendService.svelte.ts create mode 100644 apps/desktop/src/lib/backend/index.ts create mode 100644 apps/desktop/src/lib/backend/web.ts diff --git a/apps/desktop/src/lib/backend/backendService.svelte.ts b/apps/desktop/src/lib/backend/backendService.svelte.ts deleted file mode 100644 index 4a98c0a9d0..0000000000 --- a/apps/desktop/src/lib/backend/backendService.svelte.ts +++ /dev/null @@ -1 +0,0 @@ -export default class BackendService {} diff --git a/apps/desktop/src/lib/backend/index.ts b/apps/desktop/src/lib/backend/index.ts new file mode 100644 index 0000000000..44427be9dd --- /dev/null +++ b/apps/desktop/src/lib/backend/index.ts @@ -0,0 +1,84 @@ +import { + tauriInvoke, + tauriListen, + tauriCheck, + tauriGetVersion, + tauriReadFile +} from '$lib/backend/tauri'; +import { + webCheckUpdate, + webCurrentVersion, + webInvoke, + webListen, + webReadFile +} from '$lib/backend/web'; + +type Event = { + /** Event name */ + event: string; + /** Event identifier used to unlisten */ + id: number; + /** Event payload */ + payload: T; +}; + +export type DownloadEvent = + | { + event: 'Started'; + data: { + contentLength?: number; + }; + } + | { + event: 'Progress'; + data: { + chunkLength: number; + }; + } + | { + event: 'Finished'; + }; + +export type DownloadEventName = DownloadEvent['event']; + +export type DownloadUpdate = (onEvent?: (progress: DownloadEvent) => void) => Promise; +export type InstallUpdate = () => Promise; + +export type Update = { + version: string; + currentVersion: string; + body?: string; + download: DownloadUpdate; + install: InstallUpdate; +}; + +export interface IBackend { + invoke: (command: string, ...args: any[]) => Promise; + listen: (event: string, callback: (event: Event) => void) => () => Promise; + checkUpdate: () => Promise; + currentVersion: () => Promise; + readFile: (path: string) => Promise; +} + +class Tauri implements IBackend { + invoke = tauriInvoke; + listen = tauriListen; + checkUpdate = tauriCheck; + currentVersion = tauriGetVersion; + readFile = tauriReadFile; +} + +class Web implements IBackend { + invoke = webInvoke; + listen = webListen; + checkUpdate = webCheckUpdate; + currentVersion = webCurrentVersion; + readFile = webReadFile; +} + +export default function createBackend(): IBackend { + if (import.meta.env.VITE_BUILD_TARGET === 'web') { + return new Web(); + } + return new Tauri(); +} diff --git a/apps/desktop/src/lib/backend/ipc.ts b/apps/desktop/src/lib/backend/ipc.ts index 357ce83c4c..9ce161d2f7 100644 --- a/apps/desktop/src/lib/backend/ipc.ts +++ b/apps/desktop/src/lib/backend/ipc.ts @@ -1,19 +1,4 @@ import { Code } from '$lib/error/knownErrors'; -import { isReduxError } from '$lib/state/reduxError'; -import { getCookie } from '$lib/utils/cookies'; -import { invoke as invokeTauri } from '@tauri-apps/api/core'; -import { listen as listenTauri } from '@tauri-apps/api/event'; -import type { EventCallback, EventName } from '@tauri-apps/api/event'; - -type ServerResonse = - | { - type: 'success'; - subject: T; - } - | { - type: 'error'; - subject: unknown; - }; export class UserError extends Error { code!: Code; @@ -40,104 +25,3 @@ export function getUserErrorCode(error: unknown): Code | undefined { const userError = UserError.fromError(error); return userError.code; } - -export async function invoke(command: string, params: Record = {}): Promise { - // This commented out code can be used to delay/reject an api call - // return new Promise((resolve, reject) => { - // if (command.startsWith('apply')) { - // setTimeout(() => { - // reject('testing the error page'); - // }, 500); - // } else { - // resolve(invokeTauri(command, params)); - // } - // }).catch((reason) => { - // const userError = UserError.fromError(reason); - // console.error(`ipc->${command}: ${JSON.stringify(params)}`, userError); - // throw userError; - // }); - - try { - if (import.meta.env.VITE_BUILD_TARGET === 'web') { - const response = await fetch(`http://${getWebUrl()}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ command, params }) - }); - const out: ServerResonse = await response.json(); - if (out.type === 'success') { - return out.subject; - } else { - if (isReduxError(out.subject)) { - console.error(`ipc->${command}: ${JSON.stringify(params)}`, out.subject); - } - throw out.subject; - } - } else { - return await invokeTauri(command, params); - } - } catch (error: unknown) { - if (isReduxError(error)) { - console.error(`ipc->${command}: ${JSON.stringify(params)}`, error); - } - throw error; - } -} - -let webListener: WebListener | undefined; - -export function listen(event: EventName, handle: EventCallback) { - if (import.meta.env.VITE_BUILD_TARGET === 'web') { - if (!webListener) { - webListener = new WebListener(); - } - - // TODO: Listening in electron - return webListener.listen({ name: event, handle }); - } else { - const unlisten = listenTauri(event, handle); - return async () => await unlisten.then((unlistenFn) => unlistenFn()); - } -} - -class WebListener { - private socket: WebSocket | undefined; - private count = 0; - private handlers: { name: EventName; handle: EventCallback }[] = []; - - listen(handler: { name: EventName; handle: EventCallback }): () => Promise { - this.handlers.push(handler); - this.count++; - if (!this.socket) { - this.socket = new WebSocket(`ws://${getWebUrl()}/ws`); - this.socket.addEventListener('message', (event) => { - const data: { name: string; payload: any } = JSON.parse(event.data); - for (const handler of this.handlers) { - if (handler.name === data.name) { - // The id is an artifact from tauri, we don't use it so - // I've used a random value - handler.handle({ event: data.name, payload: data.payload, id: 69 }); - } - } - }); - } - - // This needs to be async just so it's the same API as the tauri version - return async () => { - this.handlers = this.handlers.filter((h) => h !== handler); - this.count--; - if (this.count === 0) { - this.socket?.close(); - this.socket = undefined; - } - }; - } -} - -function getWebUrl(): string { - const host = getCookie('butlerHost') || import.meta.env.VITE_BUTLER_HOST || 'localhost'; - const port = getCookie('butlerPort') || import.meta.env.VITE_BUTLER_PORT || '6978'; - return `${host}:${port}`; -} diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index df857c60c2..d0950cfa30 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -1,10 +1,42 @@ -import { invoke as invokeIpc, listen as listenIpc } from '$lib/backend/ipc'; -import { getVersion } from '@tauri-apps/api/app'; -import { check } from '@tauri-apps/plugin-updater'; - -export class Tauri { - invoke = invokeIpc; - listen = listenIpc; - checkUpdate = check; - currentVersion = getVersion; +import { isReduxError } from '$lib/state/reduxError'; +import { invoke as invokeTauri } from '@tauri-apps/api/core'; +import { listen as listenTauri } from '@tauri-apps/api/event'; +import type { EventCallback, EventName } from '@tauri-apps/api/event'; + +export async function tauriInvoke( + command: string, + params: Record = {} +): Promise { + // This commented out code can be used to delay/reject an api call + // return new Promise((resolve, reject) => { + // if (command.startsWith('apply')) { + // setTimeout(() => { + // reject('testing the error page'); + // }, 500); + // } else { + // resolve(invokeTauri(command, params)); + // } + // }).catch((reason) => { + // const userError = UserError.fromError(reason); + // console.error(`ipc->${command}: ${JSON.stringify(params)}`, userError); + // throw userError; + // }); + + try { + return await invokeTauri(command, params); + } catch (error: unknown) { + if (isReduxError(error)) { + console.error(`ipc->${command}: ${JSON.stringify(params)}`, error); + } + throw error; + } } + +export function tauriListen(event: EventName, handle: EventCallback) { + const unlisten = listenTauri(event, handle); + return async () => await unlisten.then((unlistenFn) => unlistenFn()); +} + +export { getVersion as tauriGetVersion } from '@tauri-apps/api/app'; +export { check as tauriCheck } from '@tauri-apps/plugin-updater'; +export { readFile as tauriReadFile } from '@tauri-apps/plugin-fs'; diff --git a/apps/desktop/src/lib/backend/web.ts b/apps/desktop/src/lib/backend/web.ts new file mode 100644 index 0000000000..8644ebf755 --- /dev/null +++ b/apps/desktop/src/lib/backend/web.ts @@ -0,0 +1,142 @@ +import { isReduxError } from '$lib/state/reduxError'; + +import { getCookie } from '$lib/utils/cookies'; + +/** + * Invokes a backend web command via HTTP POST and returns the result. + * + * @template T The expected type of the response subject. + * @param command - The name of the backend command to invoke. + * @param params - An optional object containing parameters for the command. + * @returns A promise that resolves with the subject of the response if successful. + * @throws Throws an error if the backend responds with an error or if the request fails. + */ +export async function webInvoke( + command: string, + params: Record = {} +): Promise { + try { + const response = await fetch(`http://${getWebUrl()}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ command, params }) + }); + const out: ServerResonse = await response.json(); + if (out.type === 'success') { + return out.subject; + } else { + if (isReduxError(out.subject)) { + console.error(`ipc->${command}: ${JSON.stringify(params)}`, out.subject); + } + throw out.subject; + } + } catch (error: unknown) { + if (isReduxError(error)) { + console.error(`ipc->${command}: ${JSON.stringify(params)}`, error); + } + throw error; + } +} + +/** + * Registers an event listener for a specified event name using the singleton `WebListener` instance. + * + * @template T - The type of the event payload. + * @param event - The name of the event to listen for. + * @param handle - The callback function to handle the event when it is triggered. + * @returns A function or object that can be used to remove or manage the event listener, as provided by `WebListener.listen`. + */ +export function webListen(event: EventName, handle: EventCallback) { + const webListener = WebListener.getInstance(); + return webListener.listen({ name: event, handle }); +} + +export async function webCheckUpdate(): Promise { + // TODO: Implement this for the web version if needed + return null; +} + +export async function webCurrentVersion(): Promise { + // TODO: Implement this for the web version if needed + return '0.0.0'; +} + +export async function webReadFile(_path: string): Promise { + // TODO: Implement this for the web version if needed + throw new Error('webReadFile is not implemented for the web version'); +} + +class WebListener { + private socket: WebSocket | undefined; + private count = 0; + private handlers: { name: EventName; handle: EventCallback }[] = []; + private static instance: WebListener | undefined; + + private constructor() {} + + static getInstance(): WebListener { + if (!WebListener.instance) { + WebListener.instance = new WebListener(); + } + return WebListener.instance; + } + + listen(handler: { name: EventName; handle: EventCallback }): () => Promise { + this.handlers.push(handler); + this.count++; + if (!this.socket) { + this.socket = new WebSocket(`ws://${getWebUrl()}/ws`); + this.socket.addEventListener('message', (event) => { + const data: { name: string; payload: any } = JSON.parse(event.data); + for (const handler of this.handlers) { + if (handler.name === data.name) { + // The id is an artifact from tauri, we don't use it so + // I've used a random value + handler.handle({ event: data.name, payload: data.payload, id: 69 }); + } + } + }); + } + + // This needs to be async just so it's the same API as the tauri version + return async () => { + this.handlers = this.handlers.filter((h) => h !== handler); + this.count--; + if (this.count === 0) { + this.socket?.close(); + this.socket = undefined; + } + }; + } +} + +function getWebUrl(): string { + const host = getCookie('butlerHost') || import.meta.env.VITE_BUTLER_HOST || 'localhost'; + const port = getCookie('butlerPort') || import.meta.env.VITE_BUTLER_PORT || '6978'; + return `${host}:${port}`; +} + +type EventName = string; + +interface Event { + /** Event name */ + event: string; + /** Event identifier used to unlisten */ + id: number; + /** Event payload */ + payload: T; +} + +type EventCallback = (event: Event) => void; + +type ServerResonse = + | { + type: 'success'; + subject: T; + } + | { + type: 'error'; + subject: unknown; + }; From 8a7dfdff68c8dba2df437e8652f28c9c2012453b Mon Sep 17 00:00:00 2001 From: estib Date: Wed, 13 Aug 2025 15:49:14 +0200 Subject: [PATCH 2/3] Switch from Tauri backend to new Backend interface Replace all usage of the old Tauri backend instance with the new unified IBackend interface, as returned from createBackend(). Update all affected services, dependency injections, and imports so they now adhere to the new backend abstraction. Remove references to tauri where possible, and update service constructors and API calls accordingly. --- .../desktop/src/components/AppUpdater.test.ts | 43 ++++++------- apps/desktop/src/components/CloneForm.svelte | 8 +-- .../src/components/CommitSigningForm.svelte | 8 ++- apps/desktop/src/components/Feed.svelte | 8 ++- .../src/components/FileContextMenu.svelte | 6 +- .../ProjectSettingsMenuAction.svelte | 6 +- .../src/components/ShareIssueModal.svelte | 23 ++++--- .../profileSettings/CliSymLink.svelte | 14 +++-- .../profileSettings/GeneralSettings.svelte | 12 +++- apps/desktop/src/lib/ai/service.test.ts | 5 +- apps/desktop/src/lib/backend/index.ts | 8 +++ apps/desktop/src/lib/cli/cli.ts | 37 +++++++++++ apps/desktop/src/lib/codegen/claude.ts | 8 +-- .../src/lib/commits/commitService.svelte.ts | 8 ++- apps/desktop/src/lib/config/appSettingsV2.ts | 22 ++++--- .../src/lib/config/gitConfigService.ts | 22 ++++--- apps/desktop/src/lib/editorLink/editorLink.ts | 6 -- apps/desktop/src/lib/feed/feed.ts | 23 ++++--- apps/desktop/src/lib/files/fileService.ts | 14 ++++- .../forge/github/githubUserService.svelte.ts | 8 +-- apps/desktop/src/lib/git/gitService.ts | 21 ++++++- apps/desktop/src/lib/history/history.ts | 25 +++++--- apps/desktop/src/lib/hooks/hooksService.ts | 10 +-- apps/desktop/src/lib/mode/modeService.ts | 20 +++--- .../src/lib/project/projectsService.ts | 25 +++++--- apps/desktop/src/lib/prompt/promptService.ts | 9 +-- .../desktop/src/lib/remotes/remotesService.ts | 7 ++- .../src/lib/rules/rulesService.svelte.ts | 8 +-- .../desktop/src/lib/secrets/secretsService.ts | 9 ++- .../src/lib/shortcuts/shortcutService.ts | 6 +- .../src/lib/stacks/stackService.svelte.ts | 12 ++-- apps/desktop/src/lib/state/backendQuery.ts | 14 ++--- .../src/lib/state/clientState.svelte.ts | 13 ++-- apps/desktop/src/lib/support/dataSharing.ts | 35 +++++++++-- .../src/lib/testing/mockGitHubApi.svelte.ts | 6 +- apps/desktop/src/lib/testing/setup.ts | 11 ++-- apps/desktop/src/lib/updater/updater.test.ts | 45 +++++-------- apps/desktop/src/lib/updater/updater.ts | 63 +++++++++++-------- apps/desktop/src/lib/user/userService.ts | 9 +-- apps/desktop/src/lib/utils/url.ts | 5 -- .../lib/worktree/worktreeService.svelte.ts | 8 +-- apps/desktop/src/routes/+layout.svelte | 29 +++++---- apps/desktop/src/routes/+layout.ts | 20 +++--- 43 files changed, 413 insertions(+), 286 deletions(-) create mode 100644 apps/desktop/src/lib/cli/cli.ts delete mode 100644 apps/desktop/src/lib/editorLink/editorLink.ts diff --git a/apps/desktop/src/components/AppUpdater.test.ts b/apps/desktop/src/components/AppUpdater.test.ts index 1c3cd30e19..c1e2d91470 100644 --- a/apps/desktop/src/components/AppUpdater.test.ts +++ b/apps/desktop/src/components/AppUpdater.test.ts @@ -1,19 +1,18 @@ import AppUpdater from '$components/AppUpdater.svelte'; import { EventContext } from '$lib/analytics/eventContext'; import { PostHogWrapper } from '$lib/analytics/posthog'; -import { Tauri } from '$lib/backend/tauri'; +import createBackend, { type Update } from '$lib/backend'; import { ShortcutService } from '$lib/shortcuts/shortcutService'; import { getSettingsdServiceMock } from '$lib/testing/mockSettingsdService'; import { UPDATER_SERVICE, UpdaterService } from '$lib/updater/updater'; import { render, screen } from '@testing-library/svelte'; import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest'; -import type { Update } from '@tauri-apps/plugin-updater'; describe('AppUpdater', () => { let updater: UpdaterService; let context: Map; - const tauri = new Tauri(); - const shortcuts = new ShortcutService(tauri); + const backend = createBackend(); + const shortcuts = new ShortcutService(backend); const MockSettingsService = getSettingsdServiceMock(); const settingsService = new MockSettingsService(); const eventContext = new EventContext(); @@ -21,9 +20,9 @@ describe('AppUpdater', () => { beforeEach(() => { vi.useFakeTimers(); - updater = new UpdaterService(tauri, posthog, shortcuts); + updater = new UpdaterService(backend, posthog, shortcuts); context = new Map([[UPDATER_SERVICE._key, updater]]); - vi.spyOn(tauri, 'listen').mockReturnValue(async () => {}); + vi.spyOn(backend, 'listen').mockReturnValue(async () => {}); vi.mock('$env/dynamic/public', () => { return { env: { @@ -39,23 +38,18 @@ describe('AppUpdater', () => { }); test('should be hidden if no update', async () => { - vi.spyOn(tauri, 'checkUpdate').mockReturnValue( - mockUpdate({ - version: '1' - }) - ); + vi.spyOn(backend, 'checkUpdate').mockReturnValue(mockUpdate(null)); render(AppUpdater, { context }); await vi.advanceTimersToNextTimerAsync(); const updateBanner = screen.queryByTestId('update-banner'); - expect(updateBanner).toBeNull(); + expect(updateBanner).toBe(null); }); test('should display download button', async () => { - vi.spyOn(tauri, 'checkUpdate').mockReturnValue( + vi.spyOn(backend, 'checkUpdate').mockReturnValue( mockUpdate({ - available: true, version: '1', body: 'release notes' }) @@ -68,24 +62,19 @@ describe('AppUpdater', () => { expect(button).toBeVisible(); }); - test('should display up-to-date on manaul check', async () => { - vi.spyOn(tauri, 'checkUpdate').mockReturnValue( - mockUpdate({ - available: false - }) - ); - render(AppUpdater, { context }); + test('should display up-to-date on manual check', async () => { + vi.spyOn(backend, 'checkUpdate').mockReturnValue(mockUpdate(null)); + const { getByTestId } = render(AppUpdater, { context }); updater.checkForUpdate(true); await vi.advanceTimersToNextTimerAsync(); - const button = screen.getByTestId('got-it'); + const button = getByTestId('got-it'); expect(button).toBeVisible(); }); test('should display restart button on install complete', async () => { - vi.spyOn(tauri, 'checkUpdate').mockReturnValue( + vi.spyOn(backend, 'checkUpdate').mockReturnValue( mockUpdate({ - available: true, version: '2', body: 'release notes' }) @@ -102,7 +91,11 @@ describe('AppUpdater', () => { }); }); -async function mockUpdate(update: Partial): Promise { +async function mockUpdate(update: Partial | null): Promise { + if (update === null) { + return await Promise.resolve(null); + } + return await Promise.resolve({ download: () => {}, install: () => {}, diff --git a/apps/desktop/src/components/CloneForm.svelte b/apps/desktop/src/components/CloneForm.svelte index 4f82b61138..ffc10db994 100644 --- a/apps/desktop/src/components/CloneForm.svelte +++ b/apps/desktop/src/components/CloneForm.svelte @@ -3,7 +3,7 @@ import InfoMessage, { type MessageStyle } from '$components/InfoMessage.svelte'; import Section from '$components/Section.svelte'; import { POSTHOG_WRAPPER } from '$lib/analytics/posthog'; - import { invoke } from '$lib/backend/ipc'; + import { GIT_SERVICE } from '$lib/git/gitService'; import { PROJECTS_SERVICE } from '$lib/project/projectsService'; import { projectPath } from '$lib/routes/routes.svelte'; import { parseRemoteUrl } from '$lib/url/gitUrl'; @@ -18,6 +18,7 @@ import { onMount } from 'svelte'; const projectsService = inject(PROJECTS_SERVICE); + const gitService = inject(GIT_SERVICE); const posthog = inject(POSTHOG_WRAPPER); let loading = $state(false); @@ -84,10 +85,7 @@ const targetDir = await join(targetDirPath, remoteUrl.name); - await invoke('git_clone_repository', { - repositoryUrl, - targetDir - }); + await gitService.cloneRepo(repositoryUrl, targetDir); posthog.capture('Repository Cloned', { protocol: remoteUrl.protocol }); const project = await projectsService.addProject(targetDir); diff --git a/apps/desktop/src/components/CommitSigningForm.svelte b/apps/desktop/src/components/CommitSigningForm.svelte index 2d53bee214..624aa8aed0 100644 --- a/apps/desktop/src/components/CommitSigningForm.svelte +++ b/apps/desktop/src/components/CommitSigningForm.svelte @@ -2,8 +2,8 @@ import InfoMessage from '$components/InfoMessage.svelte'; import Section from '$components/Section.svelte'; import SectionCardDisclaimer from '$components/SectionCardDisclaimer.svelte'; - import { invoke } from '$lib/backend/ipc'; import { GIT_CONFIG_SERVICE } from '$lib/config/gitConfigService'; + import { GIT_SERVICE } from '$lib/git/gitService'; import { inject } from '@gitbutler/shared/context'; import { Button, Link, SectionCard, Select, SelectItem, Textbox, Toggle } from '@gitbutler/ui'; @@ -12,6 +12,7 @@ const { projectId }: { projectId: string } = $props(); const gitConfig = inject(GIT_CONFIG_SERVICE); + const gitService = inject(GIT_SERVICE); let signCommits = $state(false); @@ -57,8 +58,9 @@ errorMessage = ''; checked = true; loading = true; - await invoke('check_signing_settings', { projectId: projectId }) - .then((_) => { + await gitService + .checkSigningSettings(projectId) + .then(() => { signCheckResult = true; }) .catch((err) => { diff --git a/apps/desktop/src/components/Feed.svelte b/apps/desktop/src/components/Feed.svelte index 930bcc9e39..4404976e22 100644 --- a/apps/desktop/src/components/Feed.svelte +++ b/apps/desktop/src/components/Feed.svelte @@ -4,7 +4,7 @@ import CliSymLink from '$components/profileSettings/CliSymLink.svelte'; import { ACTION_SERVICE } from '$lib/actions/actionService.svelte'; import laneNewSvg from '$lib/assets/empty-state/lane-new.svg?raw'; - import { invoke } from '$lib/backend/ipc'; + import { CLI_MANAGER } from '$lib/cli/cli'; import { SETTINGS_SERVICE } from '$lib/config/appSettingsV2'; import { projectAiGenEnabled } from '$lib/config/config'; import { FEED_FACTORY } from '$lib/feed/feed'; @@ -26,6 +26,9 @@ const settingsService = inject(SETTINGS_SERVICE); const settingsStore = $derived(settingsService.appSettings); + const cliManager = inject(CLI_MANAGER); + const [instalCLI, installingCLI] = cliManager.install; + const combinedEntries = $derived(feed.combined); const lastAddedId = $derived(feed.lastAddedId); @@ -202,7 +205,8 @@ kind="outline" icon="play" size="tag" - onclick={async () => await invoke('install_cli')}>Install But CLI await instalCLI()}>Install But CLI (requires admin) or diff --git a/apps/desktop/src/components/FileContextMenu.svelte b/apps/desktop/src/components/FileContextMenu.svelte index 8db2d4beca..d4f4f81c0e 100644 --- a/apps/desktop/src/components/FileContextMenu.svelte +++ b/apps/desktop/src/components/FileContextMenu.svelte @@ -6,6 +6,7 @@ import { writeClipboard } from '$lib/backend/clipboard'; import { changesToDiffSpec } from '$lib/commits/utils'; import { projectAiExperimentalFeaturesEnabled, projectAiGenEnabled } from '$lib/config/config'; + import { FILE_SERVICE } from '$lib/files/fileService'; import { isTreeChange, type TreeChange } from '$lib/hunks/change'; import { platformName } from '$lib/platform/platform'; import { vscodePath } from '$lib/project/project'; @@ -15,7 +16,7 @@ import { STACK_SERVICE } from '$lib/stacks/stackService.svelte'; import { UI_STATE } from '$lib/state/uiState.svelte'; import { computeChangeStatus } from '$lib/utils/fileStatus'; - import { getEditorUri, openExternalUrl, showFileInFolder } from '$lib/utils/url'; + import { getEditorUri, openExternalUrl } from '$lib/utils/url'; import { inject } from '@gitbutler/shared/context'; import { @@ -61,6 +62,7 @@ const idSelection = inject(ID_SELECTION); const aiService = inject(AI_SERVICE); const actionService = inject(ACTION_SERVICE); + const fileService = inject(FILE_SERVICE); const [autoCommit, autoCommitting] = actionService.autoCommit; const [branchChanges, branchingChanges] = actionService.branchChanges; const [absorbChanges, absorbingChanges] = actionService.absorb; @@ -446,7 +448,7 @@ const projectPath = project?.path; if (projectPath) { const absPath = await join(projectPath, item.changes[0]!.path); - await showFileInFolder(absPath); + await fileService.showFileInFolder(absPath); } contextMenu.close(); }} diff --git a/apps/desktop/src/components/ProjectSettingsMenuAction.svelte b/apps/desktop/src/components/ProjectSettingsMenuAction.svelte index ddd8e2a19c..3332bf433c 100644 --- a/apps/desktop/src/components/ProjectSettingsMenuAction.svelte +++ b/apps/desktop/src/components/ProjectSettingsMenuAction.svelte @@ -1,12 +1,13 @@