diff --git a/packages/backend/src/managers/applicationManager.ts b/packages/backend/src/managers/applicationManager.ts index 3f52ae72c..325c07fcc 100644 --- a/packages/backend/src/managers/applicationManager.ts +++ b/packages/backend/src/managers/applicationManager.ts @@ -32,6 +32,7 @@ import type { ModelsManager } from './modelsManager'; import { getPortsInfo } from '../utils/ports'; import { goarch } from '../utils/arch'; import { getDurationSecondsSince, isEndpointAlive, timeout } from '../utils/utils'; +import type { LocalRepositoryRegistry } from '../registries/LocalRepositoryRegistry'; export const LABEL_RECIPE_ID = 'ai-studio-recipe-id'; @@ -69,6 +70,7 @@ export class ApplicationManager { private recipeStatusRegistry: RecipeStatusRegistry, private modelsManager: ModelsManager, private telemetry: TelemetryLogger, + private localRepositories: LocalRepositoryRegistry, ) {} async pullApplication(recipe: Recipe, model: ModelInfo) { @@ -86,6 +88,10 @@ export class ApplicationManager { targetDirectory: localFolder, }; await this.doCheckout(gitCloneInfo, taskUtil); + this.localRepositories.register({ + path: gitCloneInfo.targetDirectory, + recipeId: recipe.id, + }); // load and parse the recipe configuration file and filter containers based on architecture, gpu accelerator // and backend (that define which model supports) diff --git a/packages/backend/src/registries/LocalRepositoryRegistry.ts b/packages/backend/src/registries/LocalRepositoryRegistry.ts new file mode 100644 index 000000000..390d0dbbc --- /dev/null +++ b/packages/backend/src/registries/LocalRepositoryRegistry.ts @@ -0,0 +1,41 @@ +import type { LocalRepository } from '@shared/src/models/ILocalRepository'; +import * as podmanDesktopApi from '@podman-desktop/api'; +import { MSG_LOCAL_REPOSITORY_UPDATE } from '@shared/Messages'; +import type { Webview } from '@podman-desktop/api'; + +/** + * The LocalRepositoryRegistry is responsible for keeping track of the directories where recipe are cloned + */ +export class LocalRepositoryRegistry { + // Map path => LocalRepository + private repositories: Map = new Map(); + + constructor(private webview: Webview) {} + + register(localRepository: LocalRepository): podmanDesktopApi.Disposable { + this.repositories.set(localRepository.path, localRepository); + this.notify(); + + return podmanDesktopApi.Disposable.create(() => { + this.unregister(localRepository.path); + }); + } + + unregister(path: string): void { + this.repositories.delete(path); + this.notify(); + } + + getLocalRepositories(): LocalRepository[] { + return Object.values(this.repositories); + } + + private notify() { + this.webview.postMessage({ + id: MSG_LOCAL_REPOSITORY_UPDATE, + body: this.getLocalRepositories(), + }).catch((err: unknown) => { + console.error('Something went wrong while notifying local repositories update', err); + }); + } +} diff --git a/packages/backend/src/studio-api-impl.ts b/packages/backend/src/studio-api-impl.ts index b59496a12..812a82a46 100644 --- a/packages/backend/src/studio-api-impl.ts +++ b/packages/backend/src/studio-api-impl.ts @@ -31,6 +31,8 @@ import type { PlaygroundState } from '@shared/src/models/IPlaygroundState'; import type { ModelsManager } from './managers/modelsManager'; import type { EnvironmentState } from '@shared/src/models/IEnvironmentState'; import type { EnvironmentManager } from './managers/environmentManager'; +import type { LocalRepository } from '@shared/src/models/ILocalRepository'; +import type { LocalRepositoryRegistry } from './registries/LocalRepositoryRegistry'; export class StudioApiImpl implements StudioAPI { constructor( @@ -41,6 +43,7 @@ export class StudioApiImpl implements StudioAPI { private modelsManager: ModelsManager, private environmentManager: EnvironmentManager, private telemetry: podmanDesktopApi.TelemetryLogger, + private localRepositories: LocalRepositoryRegistry, ) {} async ping(): Promise { @@ -194,4 +197,8 @@ export class StudioApiImpl implements StudioAPI { ): Promise { this.telemetry.logError(eventName, data); } + + async getLocalRepositories(): Promise { + return this.localRepositories.getLocalRepositories(); + } } diff --git a/packages/backend/src/studio.ts b/packages/backend/src/studio.ts index be7d091aa..b6c864d89 100644 --- a/packages/backend/src/studio.ts +++ b/packages/backend/src/studio.ts @@ -39,6 +39,7 @@ import fs from 'node:fs'; import { ContainerRegistry } from './registries/ContainerRegistry'; import { PodmanConnection } from './managers/podmanConnection'; import { EnvironmentManager } from './managers/environmentManager'; +import { LocalRepositoryRegistry } from './registries/LocalRepositoryRegistry'; // TODO: Need to be configured export const AI_STUDIO_FOLDER = path.join('podman-desktop', 'ai-studio'); @@ -129,12 +130,15 @@ export class Studio { // Create catalog manager, responsible for loading the catalog files and watching for changes this.catalogManager = new CatalogManager(appUserDirectory, this.#panel.webview); this.modelsManager = new ModelsManager(appUserDirectory, this.#panel.webview, this.catalogManager, this.telemetry); + + const localRepositoryRegistry = new LocalRepositoryRegistry(this.#panel.webview); const applicationManager = new ApplicationManager( appUserDirectory, gitManager, recipeStatusRegistry, this.modelsManager, this.telemetry, + localRepositoryRegistry, ); const envManager = new EnvironmentManager(this.#panel.webview, podmanConnection); @@ -151,6 +155,7 @@ export class Studio { this.modelsManager, envManager, this.telemetry, + localRepositoryRegistry, ); await this.catalogManager.loadCatalog(); diff --git a/packages/frontend/src/stores/localRepositories.ts b/packages/frontend/src/stores/localRepositories.ts new file mode 100644 index 000000000..b1289dc1b --- /dev/null +++ b/packages/frontend/src/stores/localRepositories.ts @@ -0,0 +1,21 @@ +import type { Readable } from 'svelte/store'; +import { readable } from 'svelte/store'; +import { MSG_LOCAL_REPOSITORY_UPDATE, MSG_NEW_RECIPE_STATE } from '@shared/Messages'; +import { rpcBrowser, studioClient } from '/@/utils/client'; +import type { LocalRepository } from '@shared/src/models/ILocalRepository'; + +export const localRepositories: Readable = readable( + [], + set => { + const sub = rpcBrowser.subscribe(MSG_LOCAL_REPOSITORY_UPDATE, msg => { + set(msg); + }); + // Initialize the store manually + studioClient.getLocalRepositories().then(state => { + set(state); + }); + return () => { + sub.unsubscribe(); + }; + }, +); diff --git a/packages/shared/Messages.ts b/packages/shared/Messages.ts index ef748c6cc..318d1154e 100644 --- a/packages/shared/Messages.ts +++ b/packages/shared/Messages.ts @@ -4,4 +4,5 @@ export const MSG_NEW_CATALOG_STATE = 'new-catalog-state'; export const MSG_NEW_RECIPE_STATE = 'new-recipe-state'; export const MSG_NEW_MODELS_STATE = 'new-models-state'; export const MSG_ENVIRONMENTS_STATE_UPDATE = 'environments-state-update'; +export const MSG_LOCAL_REPOSITORY_UPDATE = 'local-repository-update'; diff --git a/packages/shared/src/StudioAPI.ts b/packages/shared/src/StudioAPI.ts index 5fe79ef02..baf53083c 100644 --- a/packages/shared/src/StudioAPI.ts +++ b/packages/shared/src/StudioAPI.ts @@ -5,6 +5,7 @@ import type { Catalog } from './models/ICatalog'; import type { PlaygroundState } from './models/IPlaygroundState'; import type { TelemetryTrustedValue } from '@podman-desktop/api'; import type { EnvironmentState } from './models/IEnvironmentState'; +import type { LocalRepository } from './models/ILocalRepository'; export abstract class StudioAPI { abstract ping(): Promise; @@ -34,4 +35,6 @@ export abstract class StudioAPI { abstract telemetryLogUsage(eventName: string, data?: Record): Promise; abstract telemetryLogError(eventName: string, data?: Record): Promise; + + abstract getLocalRepositories(): Promise; } diff --git a/packages/shared/src/models/ILocalRepository.ts b/packages/shared/src/models/ILocalRepository.ts new file mode 100644 index 000000000..802b96bb8 --- /dev/null +++ b/packages/shared/src/models/ILocalRepository.ts @@ -0,0 +1,4 @@ +export interface LocalRepository { + path: string; + recipeId: string; +}