@7n/tauri-components 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/tauri-components",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Shared LLM agent engine + Vue/Quasar UI for Tauri apps (chat, journal, trust-tier approval).",
6
6
  "license": "MIT",
@@ -19,7 +19,8 @@
19
19
  "default": "./src/index.js"
20
20
  },
21
21
  "./vue": "./src/vue/index.js",
22
- "./components": "./src/components/index.js"
22
+ "./components": "./src/components/index.js",
23
+ "./testing": "./src/testing/index.js"
23
24
  },
24
25
  "files": [
25
26
  "src",
@@ -35,12 +36,14 @@
35
36
  "peerDependencies": {
36
37
  "@tauri-apps/api": "^2.0.0",
37
38
  "@tauri-apps/plugin-http": "^2.0.0",
39
+ "@vue/test-utils": "^2.0.0",
38
40
  "quasar": "^2.0.0",
39
41
  "vue": "^3.0.0"
40
42
  },
41
43
  "peerDependenciesMeta": {
42
44
  "@tauri-apps/api": { "optional": true },
43
45
  "@tauri-apps/plugin-http": { "optional": true },
46
+ "@vue/test-utils": { "optional": true },
44
47
  "quasar": { "optional": true }
45
48
  },
46
49
  "knip": {
@@ -14,7 +14,21 @@
14
14
 
15
15
  <template v-if="showConfig">
16
16
  <q-input v-model="baseUrl" dense outlined label="omlx base URL" />
17
- <q-input v-model="model" dense outlined label="model" />
17
+ <q-select
18
+ v-model="model"
19
+ :options="models"
20
+ :loading="modelsLoading"
21
+ use-input
22
+ fill-input
23
+ hide-selected
24
+ input-debounce="0"
25
+ new-value-mode="add-unique"
26
+ dense
27
+ outlined
28
+ label="model"
29
+ hint="завантажені моделі omlx; можна вписати свою"
30
+ @filter="(_, update) => update()"
31
+ />
18
32
  <q-input v-model="apiKey" dense outlined label="API key" type="password" />
19
33
  <q-separator class="q-my-sm" />
20
34
  </template>
@@ -73,7 +87,7 @@ const props = defineProps({
73
87
  const emit = defineEmits(['update:modelValue', 'ran'])
74
88
 
75
89
  const $q = useQuasar()
76
- const { baseUrl, model, apiKey, saveOmlx, loadOmlxEnv, request, respond } = props.agent
90
+ const { baseUrl, model, apiKey, saveOmlx, loadOmlxEnv, listModels, request, respond } = props.agent
77
91
 
78
92
  const prompt = ref('')
79
93
  const running = ref(false)
@@ -81,6 +95,8 @@ const turns = ref([])
81
95
  const requestId = ref(null)
82
96
  const logEl = ref(null)
83
97
  const showConfig = ref(false)
98
+ const models = ref([])
99
+ const modelsLoading = ref(false)
84
100
 
85
101
  // Labels shift once a conversation is under way (fresh request → follow-up).
86
102
  const inputLabel = computed(() => (turns.value.length ? 'Повідомлення' : 'Prompt'))
@@ -95,6 +111,16 @@ async function onShow() {
95
111
  turns.value = []
96
112
  requestId.value = null
97
113
  await loadOmlxEnv()
114
+ // Populate the model dropdown from the omlx server (best-effort; stays editable).
115
+ if (listModels) {
116
+ modelsLoading.value = true
117
+ try {
118
+ models.value = await listModels()
119
+ }
120
+ finally {
121
+ modelsLoading.value = false
122
+ }
123
+ }
98
124
  }
99
125
 
100
126
  /**
@@ -0,0 +1,34 @@
1
+ // List the models a local omlx (OpenAI-compatible) server has loaded, via
2
+ // GET {baseUrl}/models. Generic OpenAI shape: { data: [{ id }] }. Returns [] on
3
+ // any failure so a model picker degrades gracefully to manual entry.
4
+
5
+ /**
6
+ * @param {{ baseUrl?: string, apiKey?: string, fetchFn?: typeof fetch, signal?: AbortSignal }} [params] config
7
+ * @returns {Promise<string[]>} loaded model ids (empty on error)
8
+ */
9
+ export async function listOmlxModels({ baseUrl, apiKey, fetchFn = fetch, signal } = {}) {
10
+ if (!baseUrl) return []
11
+ try {
12
+ const response = await fetchFn(`${baseUrl}/models`, {
13
+ headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
14
+ signal,
15
+ })
16
+ if (!response.ok) return []
17
+ const data = await response.json()
18
+ return Array.isArray(data?.data) ? data.data.map(m => m?.id).filter(Boolean) : []
19
+ }
20
+ catch {
21
+ return []
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Pick a model: the preferred one if loaded, else the first available, else ''.
27
+ * @param {string[]} models loaded model ids
28
+ * @param {string} [preferred] preferred id
29
+ * @returns {string} chosen model id
30
+ */
31
+ export function resolveModel(models, preferred) {
32
+ if (preferred && models.includes(preferred)) return preferred
33
+ return models[0] ?? ''
34
+ }
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ export { createAgentKit } from './core/agent-kit.js'
7
7
  export { handleApprove, handleRequest, handleRespond } from './core/agent-handler.js'
8
8
  export { createDispatch, validateInput } from './core/dispatch.js'
9
9
  export { createOpenAiChat, runAgent } from './core/llm.js'
10
+ export { listOmlxModels, resolveModel } from './core/omlx-models.js'
10
11
  export { listTools, toJsonSchema, toolManifest } from './core/manifest.js'
11
12
  export { classify, DEFAULT_ACTOR_TIERS, scopedManifest, scopedToolNames } from './core/scope.js'
12
13
  export { getTool } from './core/tools.js'
@@ -0,0 +1,3 @@
1
+ // `@7n/tauri-components/testing` — vitest helpers for Quasar component tests.
2
+ // Requires the host app's devDeps to provide @vue/test-utils + quasar (peers).
3
+ export { mountQuasar, mountWithQuasar } from './quasar.js'
@@ -0,0 +1,48 @@
1
+ import { h } from 'vue'
2
+ import { mount } from '@vue/test-utils'
3
+ import * as Quasar from 'quasar'
4
+
5
+ // Vitest helpers for mounting components that use Quasar. Registers every Quasar
6
+ // component (QBtn, QPage, …) globally so both template usage and programmatic
7
+ // h(Quasar.QLayout) resolve in mounted test trees. Identical across consumer
8
+ // apps — import from '@7n/tauri-components/testing' instead of copying it.
9
+
10
+ const QUASAR_COMPONENT_RE = /^Q[A-Z]/
11
+ const quasarComponents = Object.fromEntries(Object.entries(Quasar).filter(([name]) => QUASAR_COMPONENT_RE.test(name)))
12
+
13
+ /**
14
+ * @param {object} [userGlobal] caller's `global` mount option
15
+ * @returns {object} `global` option with Quasar plugin + components merged in
16
+ */
17
+ function quasarGlobal(userGlobal = {}) {
18
+ return {
19
+ ...userGlobal,
20
+ components: { ...quasarComponents, ...userGlobal.components },
21
+ plugins: [...(userGlobal.plugins || []), [Quasar.Quasar, { config: { dark: false } }]],
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Mount a component with Quasar registered, without any layout wrapper.
27
+ * @param {object} component Vue component (e.g. one that renders its own QLayout)
28
+ * @param {object} [options] mount options (forwarded)
29
+ * @returns {object} test wrapper
30
+ */
31
+ export function mountQuasar(component, options = {}) {
32
+ return mount(component, { ...options, global: quasarGlobal(options.global) })
33
+ }
34
+
35
+ /**
36
+ * Mount a page-level component wrapped in QLayout > QPageContainer.
37
+ * @param {object} component Vue component
38
+ * @param {object} [options] mount options (forwarded)
39
+ * @returns {object} test wrapper
40
+ */
41
+ export function mountWithQuasar(component, options = {}) {
42
+ const wrapper = {
43
+ render() {
44
+ return h(Quasar.QLayout, { view: 'hHh lpR fFf' }, () => h(Quasar.QPageContainer, () => h(component)))
45
+ },
46
+ }
47
+ return mount(wrapper, { ...options, global: quasarGlobal(options.global) })
48
+ }
@@ -1,6 +1,7 @@
1
1
  import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
2
2
  import { createAgentKit } from '../core/agent-kit.js'
3
3
  import { createOpenAiChat } from '../core/llm.js'
4
+ import { listOmlxModels } from '../core/omlx-models.js'
4
5
  import { createTauriJournalStore } from './journal-store-tauri.js'
5
6
  import { tauriTransport } from './transports.js'
6
7
  import { useOmlx } from './use-omlx.js'
@@ -50,6 +51,7 @@ export function useAgent({ catalog, systemPrompt, grounding, actorTiers, actor =
50
51
  apiKey,
51
52
  saveOmlx: save,
52
53
  loadOmlxEnv: loadEnv,
54
+ listModels: () => listOmlxModels({ baseUrl: baseUrl.value, apiKey: apiKey.value || undefined, fetchFn: tauriFetch }),
53
55
  journal,
54
56
  request: intent => kit.request({ intent, actor, chat: chat() }),
55
57
  respond: (requestId, message) => kit.respond({ requestId, message, actor, chat: chat() }),
@@ -14,6 +14,35 @@ import { invoke } from '@tauri-apps/api/core'
14
14
 
15
15
  const DEFAULT_BASE_URL = 'http://127.0.0.1:8000/v1'
16
16
 
17
+ /**
18
+ * Read a localStorage value, or null when localStorage is unavailable
19
+ * (component tests without a DOM store, SSR).
20
+ * @param {string} key storage key
21
+ * @returns {string|null} stored value, or null
22
+ */
23
+ function readStored(key) {
24
+ try {
25
+ return globalThis.localStorage?.getItem(key) ?? null
26
+ }
27
+ catch {
28
+ return null
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Write a localStorage value; no-op when localStorage is unavailable.
34
+ * @param {string} key storage key
35
+ * @param {string} value value to store
36
+ */
37
+ function writeStored(key, value) {
38
+ try {
39
+ globalThis.localStorage?.setItem(key, value)
40
+ }
41
+ catch {
42
+ // no localStorage (tests / SSR) — in-memory ref state is still updated
43
+ }
44
+ }
45
+
17
46
  /**
18
47
  * @param {{ storagePrefix?: string, defaultBaseUrl?: string, defaultModel?: string }} [options] config
19
48
  * @returns {{ baseUrl: import('vue').Ref<string>, model: import('vue').Ref<string>, apiKey: import('vue').Ref<string>, save: () => void, loadEnv: () => Promise<void> }} persisted omlx config, an env loader and a saver
@@ -22,8 +51,8 @@ export function useOmlx({ storagePrefix = 'agent', defaultBaseUrl = DEFAULT_BASE
22
51
  const baseUrlKey = `${storagePrefix}:omlxBaseUrl`
23
52
  const modelKey = `${storagePrefix}:omlxModel`
24
53
 
25
- const baseUrl = ref(localStorage.getItem(baseUrlKey) || defaultBaseUrl)
26
- const model = ref(localStorage.getItem(modelKey) || defaultModel)
54
+ const baseUrl = ref(readStored(baseUrlKey) || defaultBaseUrl)
55
+ const model = ref(readStored(modelKey) || defaultModel)
27
56
  // Filled from the global env in loadEnv(); never persisted to localStorage.
28
57
  const apiKey = ref('')
29
58
 
@@ -43,8 +72,8 @@ export function useOmlx({ storagePrefix = 'agent', defaultBaseUrl = DEFAULT_BASE
43
72
  }
44
73
  if (!env) return
45
74
  if (env.apiKey) apiKey.value = env.apiKey
46
- if (env.baseUrl && !localStorage.getItem(baseUrlKey)) baseUrl.value = env.baseUrl
47
- if (env.model && !localStorage.getItem(modelKey)) model.value = env.model
75
+ if (env.baseUrl && !readStored(baseUrlKey)) baseUrl.value = env.baseUrl
76
+ if (env.model && !readStored(modelKey)) model.value = env.model
48
77
  }
49
78
 
50
79
  /**
@@ -52,8 +81,8 @@ export function useOmlx({ storagePrefix = 'agent', defaultBaseUrl = DEFAULT_BASE
52
81
  * persisted — it comes from the global OMLX_API_KEY env on each launch.
53
82
  */
54
83
  function save() {
55
- localStorage.setItem(baseUrlKey, baseUrl.value)
56
- localStorage.setItem(modelKey, model.value)
84
+ writeStored(baseUrlKey, baseUrl.value)
85
+ writeStored(modelKey, model.value)
57
86
  }
58
87
 
59
88
  return { baseUrl, model, apiKey, save, loadEnv }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @param {{ baseUrl?: string, apiKey?: string, fetchFn?: typeof fetch, signal?: AbortSignal }} [params] config
3
+ * @returns {Promise<string[]>} loaded model ids (empty on error)
4
+ */
5
+ export function listOmlxModels({ baseUrl, apiKey, fetchFn, signal }?: {
6
+ baseUrl?: string;
7
+ apiKey?: string;
8
+ fetchFn?: typeof fetch;
9
+ signal?: AbortSignal;
10
+ }): Promise<string[]>;
11
+ /**
12
+ * Pick a model: the preferred one if loaded, else the first available, else ''.
13
+ * @param {string[]} models loaded model ids
14
+ * @param {string} [preferred] preferred id
15
+ * @returns {string} chosen model id
16
+ */
17
+ export function resolveModel(models: string[], preferred?: string): string;
package/types/index.d.ts CHANGED
@@ -3,5 +3,6 @@ export { getTool } from "./core/tools.js";
3
3
  export { handleApprove, handleRequest, handleRespond } from "./core/agent-handler.js";
4
4
  export { createDispatch, validateInput } from "./core/dispatch.js";
5
5
  export { createOpenAiChat, runAgent } from "./core/llm.js";
6
+ export { listOmlxModels, resolveModel } from "./core/omlx-models.js";
6
7
  export { listTools, toJsonSchema, toolManifest } from "./core/manifest.js";
7
8
  export { classify, DEFAULT_ACTOR_TIERS, scopedManifest, scopedToolNames } from "./core/scope.js";