mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:28:35 +02:00
When duplicating an unnamed request, the resolved name (URL fallback) was being set as the duplicate's explicit name. Now the raw name field is preserved, so unnamed duplicates continue to dynamically show the URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
7.8 KiB
TypeScript
259 lines
7.8 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
import { resolvedModelName } from "@yaakapp/app/lib/resolvedModelName";
|
|
import { AnyModel, ModelPayload } from "../bindings/gen_models";
|
|
import { modelStoreDataAtom } from "./atoms";
|
|
import { ExtractModel, JotaiStore, ModelStoreData } from "./types";
|
|
import { newStoreData } from "./util";
|
|
|
|
let _store: JotaiStore | null = null;
|
|
|
|
export function initModelStore(store: JotaiStore) {
|
|
_store = store;
|
|
|
|
getCurrentWebviewWindow()
|
|
.listen<ModelPayload>("model_write", ({ payload }) => {
|
|
if (shouldIgnoreModel(payload)) return;
|
|
|
|
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
|
if (payload.change.type === "upsert") {
|
|
return {
|
|
...prev,
|
|
[payload.model.model]: {
|
|
...prev[payload.model.model],
|
|
[payload.model.id]: payload.model,
|
|
},
|
|
};
|
|
} else {
|
|
const modelData = { ...prev[payload.model.model] };
|
|
delete modelData[payload.model.id];
|
|
return { ...prev, [payload.model.model]: modelData };
|
|
}
|
|
});
|
|
})
|
|
.catch(console.error);
|
|
}
|
|
|
|
function mustStore(): JotaiStore {
|
|
if (_store == null) {
|
|
throw new Error("Model store was not initialized");
|
|
}
|
|
|
|
return _store;
|
|
}
|
|
|
|
let _activeWorkspaceId: string | null = null;
|
|
|
|
export async function changeModelStoreWorkspace(workspaceId: string | null) {
|
|
console.log("Syncing models with new workspace", workspaceId);
|
|
const workspaceModelsStr = await invoke<string>("models_workspace_models", {
|
|
workspaceId, // NOTE: if no workspace id provided, it will just fetch global models
|
|
});
|
|
const workspaceModels = JSON.parse(workspaceModelsStr) as AnyModel[];
|
|
const data = newStoreData();
|
|
for (const model of workspaceModels) {
|
|
data[model.model][model.id] = model;
|
|
}
|
|
|
|
mustStore().set(modelStoreDataAtom, data);
|
|
|
|
console.log("Synced model store with workspace", workspaceId, data);
|
|
|
|
_activeWorkspaceId = workspaceId;
|
|
}
|
|
|
|
export function listModels<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
modelType: M | ReadonlyArray<M>,
|
|
): T[] {
|
|
let data = mustStore().get(modelStoreDataAtom);
|
|
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
|
|
return types.flatMap((t) => Object.values(data[t]) as T[]);
|
|
}
|
|
|
|
export function getModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
modelType: M | ReadonlyArray<M>,
|
|
id: string,
|
|
): T | null {
|
|
let data = mustStore().get(modelStoreDataAtom);
|
|
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
|
|
for (const t of types) {
|
|
let v = data[t][id];
|
|
if (v?.model === t) return v as T;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function getAnyModel(id: string): AnyModel | null {
|
|
let data = mustStore().get(modelStoreDataAtom);
|
|
for (const t of Object.keys(data)) {
|
|
// oxlint-disable-next-line no-explicit-any
|
|
let v = (data as any)[t]?.[id];
|
|
if (v?.model === t) return v;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function patchModelById<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
model: M,
|
|
id: string,
|
|
patch: Partial<T> | ((prev: T) => T),
|
|
): Promise<string> {
|
|
let prev = getModel<M, T>(model, id);
|
|
if (prev == null) {
|
|
throw new Error(`Failed to get model to patch id=${id} model=${model}`);
|
|
}
|
|
|
|
const newModel = typeof patch === "function" ? patch(prev) : { ...prev, ...patch };
|
|
return updateModel(newModel);
|
|
}
|
|
|
|
export async function patchModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
base: Pick<T, "id" | "model">,
|
|
patch: Partial<T>,
|
|
): Promise<string> {
|
|
return patchModelById<M, T>(base.model, base.id, patch);
|
|
}
|
|
|
|
export async function updateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
model: T,
|
|
): Promise<string> {
|
|
return invoke<string>("models_upsert", { model });
|
|
}
|
|
|
|
export async function deleteModelById<
|
|
M extends AnyModel["model"],
|
|
T extends ExtractModel<AnyModel, M>,
|
|
>(modelType: M | M[], id: string) {
|
|
let model = getModel<M, T>(modelType, id);
|
|
await deleteModel(model);
|
|
}
|
|
|
|
export async function deleteModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
model: T | null,
|
|
) {
|
|
if (model == null) {
|
|
throw new Error("Failed to delete null model");
|
|
}
|
|
await invoke<string>("models_delete", { model });
|
|
}
|
|
|
|
export function duplicateModel<M extends AnyModel["model"], T extends ExtractModel<AnyModel, M>>(
|
|
model: T | null,
|
|
) {
|
|
if (model == null) {
|
|
throw new Error("Failed to duplicate null model");
|
|
}
|
|
|
|
// If the model has an explicit (non-empty) name, try to duplicate it with a name that doesn't conflict.
|
|
// When the name is empty, keep it empty so the display falls back to the URL.
|
|
let name: string | undefined = "name" in model ? (model as { name: string }).name : undefined;
|
|
if (name) {
|
|
const existingModels = listModels(model.model);
|
|
for (let i = 0; i < 100; i++) {
|
|
const hasConflict = existingModels.some((m) => {
|
|
if ("folderId" in m && "folderId" in model && model.folderId !== m.folderId) {
|
|
return false;
|
|
} else if (resolvedModelName(m) !== name) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
if (!hasConflict) {
|
|
break;
|
|
}
|
|
|
|
// Name conflict. Try another one
|
|
const m: RegExpMatchArray | null = name.match(/ Copy( (?<n>\d+))?$/);
|
|
if (m != null && m.groups?.n == null) {
|
|
name = name.substring(0, m.index) + " Copy 2";
|
|
} else if (m != null && m.groups?.n != null) {
|
|
name = name.substring(0, m.index) + ` Copy ${parseInt(m.groups.n) + 1}`;
|
|
} else {
|
|
name = `${name} Copy`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return invoke<string>("models_duplicate", { model: { ...model, name } });
|
|
}
|
|
|
|
export async function createGlobalModel<T extends Exclude<AnyModel, { workspaceId: string }>>(
|
|
patch: Partial<T> & Pick<T, "model">,
|
|
): Promise<string> {
|
|
return invoke<string>("models_upsert", { model: patch });
|
|
}
|
|
|
|
export async function createWorkspaceModel<T extends Extract<AnyModel, { workspaceId: string }>>(
|
|
patch: Partial<T> & Pick<T, "model" | "workspaceId">,
|
|
): Promise<string> {
|
|
return invoke<string>("models_upsert", { model: patch });
|
|
}
|
|
|
|
export function replaceModelsInStore<
|
|
M extends AnyModel["model"],
|
|
T extends Extract<AnyModel, { model: M }>,
|
|
>(model: M, models: T[]) {
|
|
const newModels: Record<string, T> = {};
|
|
for (const model of models) {
|
|
newModels[model.id] = model;
|
|
}
|
|
|
|
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
|
return {
|
|
...prev,
|
|
[model]: newModels,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function mergeModelsInStore<
|
|
M extends AnyModel["model"],
|
|
T extends Extract<AnyModel, { model: M }>,
|
|
>(model: M, models: T[], filter?: (model: T) => boolean) {
|
|
mustStore().set(modelStoreDataAtom, (prev: ModelStoreData) => {
|
|
const existingModels = { ...prev[model] } as Record<string, T>;
|
|
|
|
// Merge in new models first
|
|
for (const m of models) {
|
|
existingModels[m.id] = m;
|
|
}
|
|
|
|
// Then filter out unwanted models
|
|
if (filter) {
|
|
for (const [id, m] of Object.entries(existingModels)) {
|
|
if (!filter(m)) {
|
|
delete existingModels[id];
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
...prev,
|
|
[model]: existingModels,
|
|
};
|
|
});
|
|
}
|
|
|
|
function shouldIgnoreModel({ model, updateSource }: ModelPayload) {
|
|
// Never ignore updates from non-user sources
|
|
if (updateSource.type !== "window") {
|
|
return false;
|
|
}
|
|
|
|
// Never ignore same-window updates
|
|
if (updateSource.label === getCurrentWebviewWindow().label) {
|
|
return false;
|
|
}
|
|
|
|
// Only sync models that belong to this workspace, if a workspace ID is present
|
|
if ("workspaceId" in model && model.workspaceId !== _activeWorkspaceId) {
|
|
return true;
|
|
}
|
|
|
|
if (model.model === "key_value" && model.namespace === "no_sync") {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|