From 7f80a2f9e6899bc6de845fd4258b42613ab1d486 Mon Sep 17 00:00:00 2001
From: Mohammad Azmi
Date: Mon, 26 Jan 2026 21:50:26 +0700
Subject: [PATCH 1/7] use zod for configuration schema
---
src/stores/configuration.ts | 104 +++++-------------------------
src/stores/configurationSchema.ts | 87 +++++++++++++++++++++++++
2 files changed, 102 insertions(+), 89 deletions(-)
create mode 100644 src/stores/configurationSchema.ts
diff --git a/src/stores/configuration.ts b/src/stores/configuration.ts
index 2e3a894..17d2360 100644
--- a/src/stores/configuration.ts
+++ b/src/stores/configuration.ts
@@ -17,94 +17,16 @@ import {
setData,
setEncryptedData,
} from "@beekeeperstudio/plugin";
-import {
- AvailableProviders,
- disabledModelsByDefault,
- providerConfigs,
-} from "@/config";
+import { AvailableProviders, providerConfigs } from "@/config";
import { useChatStore } from "./chat";
-
-type Model = {
- id: string;
- displayName: string;
-};
-
-type Configurable = {
- // ==== GENERAL ====
- /** Append custom instructions to the default system instructions. */
- customInstructions: string;
- /** Append custom instructions to the default system instructions.
- * It's applied based on the connection ID */
- customConnectionInstructions: {
- workspaceId: number;
- connectionId: number;
- instructions: string;
- }[];
- allowExecutionOfReadOnlyQueries: boolean;
- enableAutoCompact: boolean;
-
- // ==== MODELS ====
- /** List of disabled models by id. */
- disabledModels: { providerId: AvailableProviders; modelId: string }[];
- /** Models that are removed are not shown in the UI and cannot be enabled. */
- removedModels: { providerId: AvailableProviders; modelId: string }[];
- providers_openaiCompat_baseUrl: string;
- providers_openaiCompat_headers: string;
- providers_ollama_baseUrl: string;
- providers_ollama_headers: string;
-} & {
- // User defined models
- [K in AvailableProviders as `providers_${K}_models`]: Model[];
-};
-
-type EncryptedConfigurable = {
- "providers.openai.apiKey": string;
- "providers.anthropic.apiKey": string;
- "providers.google.apiKey": string;
- providers_openaiCompat_apiKey: string;
-};
-
-type ConfigurationState = Configurable & EncryptedConfigurable;
-
-export type ConfigurationKey = keyof ConfigurationState;
-
-const encryptedConfigKeys: (keyof EncryptedConfigurable)[] = [
- "providers.openai.apiKey",
- "providers.anthropic.apiKey",
- "providers.google.apiKey",
- "providers_openaiCompat_apiKey",
-];
-
-const defaultConfiguration: ConfigurationState = {
- // ==== GENERAL ====
- customInstructions: "",
- customConnectionInstructions: [],
- allowExecutionOfReadOnlyQueries: false,
- enableAutoCompact: true,
-
- // ==== MODELS ====
- "providers.openai.apiKey": "",
- "providers.anthropic.apiKey": "",
- "providers.google.apiKey": "",
- providers_openaiCompat_baseUrl: "",
- providers_openaiCompat_apiKey: "",
- providers_openaiCompat_headers: "",
- providers_ollama_baseUrl: "http://localhost:11434",
- providers_ollama_headers: "",
- providers_openai_models: [],
- providers_anthropic_models: [],
- providers_google_models: [],
- providers_openaiCompat_models: [],
- providers_ollama_models: [],
- disabledModels: disabledModelsByDefault,
- removedModels: [],
-};
-
-function isEncryptedConfig(
- config: string,
-): config is keyof EncryptedConfigurable {
- return encryptedConfigKeys.includes(config as keyof EncryptedConfigurable);
-}
+import {
+ Configurable,
+ ConfigurationState,
+ defaultConfiguration,
+ encryptedConfigurableSchema,
+ isEncryptedConfig,
+ Model,
+} from "./configurationSchema";
export const useConfigurationStore = defineStore("configuration", {
state: (): ConfigurationState => {
@@ -113,7 +35,9 @@ export const useConfigurationStore = defineStore("configuration", {
getters: {
apiKeyExists(): boolean {
- return encryptedConfigKeys.some((key) => this[key].trim() !== "");
+ return Object.keys(encryptedConfigurableSchema.shape).some(
+ (key) => this[key].trim() !== "",
+ );
},
getModelsByProvider: (state) => {
@@ -252,7 +176,9 @@ export const useConfigurationStore = defineStore("configuration", {
const connection = useChatStore().connectionInfo;
const connectionId = connection.id;
const workspaceId = connection.workspaceId;
- const connectionInstructions = _.cloneDeep(this.customConnectionInstructions);
+ const connectionInstructions = _.cloneDeep(
+ this.customConnectionInstructions,
+ );
const idx = connectionInstructions.findIndex(
(i) => i.connectionId === connectionId && i.workspaceId === workspaceId,
);
diff --git a/src/stores/configurationSchema.ts b/src/stores/configurationSchema.ts
new file mode 100644
index 0000000..72aa40c
--- /dev/null
+++ b/src/stores/configurationSchema.ts
@@ -0,0 +1,87 @@
+import { disabledModelsByDefault, providerConfigs } from "@/config";
+import z from "zod/v3";
+
+function zodEnumFromObjKeys(
+ obj: Record,
+): z.ZodEnum<[K, ...K[]]> {
+ const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
+ return z.enum([firstKey, ...otherKeys]);
+}
+
+const ModelSchema = z.object({
+ id: z.string(),
+ displayName: z.string(),
+});
+
+const configurableSchema = z.object({
+ // ==== GENERAL ====
+ /** Append custom instructions to the default system instructions. */
+ customInstructions: z.string().default(""),
+ /** Append custom instructions to the default system instructions.
+ * It's applied based on the connection ID */
+ customConnectionInstructions: z
+ .array(
+ z.object({
+ workspaceId: z.number(),
+ connectionId: z.number(),
+ instructions: z.string(),
+ }),
+ )
+ .default([]),
+ allowExecutionOfReadOnlyQueries: z.boolean().default(false),
+ enableAutoCompact: z.boolean().default(true),
+
+ // ==== MODELS ====
+ /** List of disabled models by id. */
+ disabledModels: z
+ .array(
+ z.object({
+ providerId: zodEnumFromObjKeys(providerConfigs),
+ modelId: z.string(),
+ }),
+ )
+ .default(disabledModelsByDefault),
+ removedModels: z
+ .array(
+ z.object({
+ providerId: zodEnumFromObjKeys(providerConfigs),
+ modelId: z.string(),
+ }),
+ )
+ .default([]),
+
+ providers_openaiCompat_baseUrl: z.string().default(""),
+ providers_openaiCompat_headers: z.string().default(""),
+ providers_ollama_baseUrl: z.string().default("http://localhost:11434"),
+ providers_ollama_headers: z.string().default(""),
+
+ providers_mock_models: z.array(ModelSchema).default([]),
+ providers_openai_models: z.array(ModelSchema).default([]),
+ providers_anthropic_models: z.array(ModelSchema).default([]),
+ providers_google_models: z.array(ModelSchema).default([]),
+ providers_openaiCompat_models: z.array(ModelSchema).default([]),
+ providers_ollama_models: z.array(ModelSchema).default([]),
+});
+
+export const encryptedConfigurableSchema = z.object({
+ "providers.openai.apiKey": z.string().default(""),
+ "providers.anthropic.apiKey": z.string().default(""),
+ "providers.google.apiKey": z.string().default(""),
+ providers_openaiCompat_apiKey: z.string().default(""),
+});
+
+export type Model = z.infer;
+export type Configurable = z.infer;
+export type ConfigurationState = z.infer &
+ z.infer;
+
+export function isEncryptedConfig(
+ config: string,
+): config is keyof z.infer {
+ return config in encryptedConfigurableSchema.shape;
+}
+
+export const defaultConfiguration = {
+ ...configurableSchema.parse({}),
+ ...encryptedConfigurableSchema.parse({}),
+};
From 69cd8292b1f945dc9c8d03795a9206cb3ec771b5 Mon Sep 17 00:00:00 2001
From: Mohammad Azmi
Date: Wed, 28 Jan 2026 21:58:34 +0700
Subject: [PATCH 2/7] add workspace connection instructions
---
package.json | 2 +-
src/App.vue | 6 +-
src/assets/styles/components/_base-input.scss | 9 +-
src/components/common/BaseInput.vue | 5 +-
.../configuration/Configuration.vue | 9 +-
.../configuration/GeneralConfiguration.vue | 40 +--------
.../InstructionsConfiguration.vue | 83 +++++++++++++++++++
src/stores/chat.ts | 26 +++++-
src/stores/configuration.ts | 33 +++++---
src/stores/configurationSchema.ts | 18 +++-
src/stores/internalData.ts | 17 +---
yarn.lock | 8 +-
12 files changed, 163 insertions(+), 93 deletions(-)
create mode 100644 src/components/configuration/InstructionsConfiguration.vue
diff --git a/package.json b/package.json
index 2889356..6622f06 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
"@ai-sdk/openai": "^3.0.8",
"@ai-sdk/openai-compatible": "^2.0.4",
"@ai-sdk/vue": "^3.0.28",
- "@beekeeperstudio/plugin": "^1.6.0",
+ "@beekeeperstudio/plugin": "^1.7.1-beta.0",
"@beekeeperstudio/ui-kit": "0.3.1",
"@langchain/core": "^0.3.61",
"@material-symbols/font-400": "^0.31.2",
diff --git a/src/App.vue b/src/App.vue
index 79e3ae8..9006de8 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -24,7 +24,7 @@ import Configuration, {
PageId as ConfigurationPageId,
} from "@/components/configuration/Configuration.vue";
import OnboardingScreen from "./components/OnboardingScreen.vue";
-import { getData, log } from "@beekeeperstudio/plugin";
+import { appStorage, log } from "@beekeeperstudio/plugin";
import { Dialog } from "primevue";
type Page = "starting" | "chat-interface";
@@ -129,10 +129,10 @@ export default {
window.location.reload();
}, reloadDelay);
try {
- await getData();
+ await appStorage.getItem("test");
} catch (e) {
} finally {
- // Cancel reload if getData() succeeds or fails quickly
+ // Cancel reload if it succeeds or fails quickly
clearTimeout(reloadTimer);
}
},
diff --git a/src/assets/styles/components/_base-input.scss b/src/assets/styles/components/_base-input.scss
index 4d1e806..f2903f4 100644
--- a/src/assets/styles/components/_base-input.scss
+++ b/src/assets/styles/components/_base-input.scss
@@ -24,7 +24,6 @@
align-items: stretch;
display: grid;
- &::after,
textarea {
width: auto;
min-width: 1rem;
@@ -33,15 +32,9 @@
font-size: 0.831rem;
padding: 0.5rem 0.75rem;
margin: 0;
- resize: none;
+ resize: vertical;
appearance: none;
}
-
- &::after {
- content: attr(data-value) " ";
- visibility: hidden;
- white-space: pre-wrap;
- }
}
&.switch {
diff --git a/src/components/common/BaseInput.vue b/src/components/common/BaseInput.vue
index 5c155f9..d59a1d3 100644
--- a/src/components/common/BaseInput.vue
+++ b/src/components/common/BaseInput.vue
@@ -10,10 +10,7 @@
>
-
@@ -95,6 +99,7 @@ export default {
},
/** If `showActions`, we'll show save and discard buttons. */
showActions: Boolean,
+ showUnsavedError: Boolean,
},
emits: ["update:modelValue", "input", "change", "click", "save", "discard"],
@@ -137,6 +142,18 @@ export default {
align-items: center;
padding-top: 0.5rem;
gap: 0.5rem;
+ font-size: 0.831rem;
+}
+
+.unsaved-error {
+ color: var(--brand-danger);
+ display: flex;
+ align-items: center;
+
+ .material-symbols-outlined {
+ font-size: 1em;
+ margin-right: 0.5ch;
+ }
}
label :deep(.material-symbols-outlined) {
diff --git a/src/components/configuration/Configuration.vue b/src/components/configuration/Configuration.vue
index e451d8c..84710b7 100644
--- a/src/components/configuration/Configuration.vue
+++ b/src/components/configuration/Configuration.vue
@@ -6,14 +6,14 @@
class="configuration"
:visible="visible"
:show-header="false"
- @update:visible="handleUpdateVisible"
+ @update:visible="updateVisible"
>