@meistrari/chat-nuxt 4.4.0-rc.6 → 4.4.0-rc.7

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.
Files changed (24) hide show
  1. package/README.md +15 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -0
  4. package/dist/runtime/embed/components/chat-embed.d.vue.ts +3 -6
  5. package/dist/runtime/embed/components/chat-embed.vue +44 -9
  6. package/dist/runtime/embed/components/chat-embed.vue.d.ts +3 -6
  7. package/dist/runtime/embed/components/meistrari-chat-embed.vue +0 -1
  8. package/dist/runtime/embed/composables/agent-harness.d.ts +15 -0
  9. package/dist/runtime/embed/composables/agent-harness.js +55 -0
  10. package/dist/runtime/embed/types.d.ts +0 -5
  11. package/dist/runtime/lightweight/embed/composables/model-selection.d.ts +1 -1
  12. package/dist/runtime/lightweight/embed/composables/model-selection.js +3 -3
  13. package/dist/runtime/lightweight/messages/composables/use-direct-stream.d.ts +16 -35
  14. package/dist/runtime/lightweight/messages/composables/use-direct-stream.js +6 -277
  15. package/dist/runtime/lightweight/messages/model-options.js +3 -3
  16. package/dist/runtime/messages/composables/use-direct-stream.d.ts +35 -2
  17. package/dist/runtime/messages/composables/use-direct-stream.js +17 -3
  18. package/dist/runtime/messages/model-availability.d.ts +3 -1
  19. package/dist/runtime/messages/model-availability.js +13 -6
  20. package/dist/runtime/shared/composables/chat-request-context.d.ts +2 -2
  21. package/dist/runtime/shared/composables/chat-request-context.js +2 -2
  22. package/package.json +1 -1
  23. package/dist/runtime/lightweight/messages/model-availability.d.ts +0 -7
  24. package/dist/runtime/lightweight/messages/model-availability.js +0 -29
package/README.md CHANGED
@@ -33,6 +33,20 @@ Runtime internals under `src/runtime/**`, generated Nuxt aliases, and workspace-
33
33
  | [Workspace settings](docs/guides/workspace-settings.md) | Settings overrides, agent runtime ownership, `<ChatConfigurationModal>` |
34
34
  | [Upgrading to 4.0](docs/guides/upgrading-to-4.md) | Required Chat API version for v2 sends, upgrade order, vue-i18n removal |
35
35
 
36
+ ## Harness selection
37
+
38
+ Select the Tela agent; its published `agent.config.json` determines the harness:
39
+
40
+ ```vue
41
+ <MeistrariChatEmbed tela-agent-id="your-agent-id" />
42
+ ```
43
+
44
+ The chat resolves the agent before mounting its UI. Explicit branches use that branch's configuration. Missing, unreadable or unsupported harnesses show an error and never fall back to Claude. The host cannot select a harness through props or HTTP headers.
45
+
46
+ Both harnesses share the chat screen, composer, navigation and state controller, while their protocol adapters and renderers remain separate. Selecting another agent clears the old UI and resolves its configuration again. The backend independently reads the configured harness for routed requests.
47
+
48
+ Requires Agent API v4’s `GET /v4/agents/{organizationName}/{repository}/harness` endpoint. Chat resolves the existing agent coordinates, then reads the configured harness directly from Agent API using the published commit or selected branch. The response exposes only the harness. Deploy the Agent API endpoint before this Chat version; no Tela API changes are required.
49
+
36
50
  ## Conversation forks
37
51
 
38
52
  Completed assistant responses offer **Continuar em nova conversa** in the three-dot
@@ -74,7 +88,7 @@ Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload
74
88
  | `initialConversationId` | `string \| null` | `null` | Conversation to open once on mount when `conversationId` is uncontrolled |
75
89
  | `conversationScope` | `string \| null` | `null` | Isolate conversation history by technical scope within the current workspace and runtime. Scoped embeds only see same-scope conversations; values are trimmed, blank becomes `null`, and the server accepts 1-200 chars from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:`, `/`, `-` |
76
90
  | `defaultConversationCreatorFilter` | `'all' \| 'mine'` | `'all'` | Initial sidebar creator filter: every conversation in scope, or only the current user's |
77
- | `availableModels` | `AvailableChatModels` | Full catalog | Non-empty ordered list of models this embed may display and execute. A missing or invalid restored selection falls back to the first entry; an empty list fails fast |
91
+ | `availableModels` | `AvailableChatModels` | Workspace catalog | Optional non-empty ordered subset of models served for the active workspace. Unknown ids fail at runtime; a restored unavailable selection stays visible and blocked until the user chooses an available model |
78
92
  | `defaultModel` | `ChatModel` | Package default | Initial model for untouched drafts. Explicit selections and existing conversations take precedence; `availableModels` still limits the selection |
79
93
  | `defaultReasoningEffort` | `ChatReasoningEffort` | Automatic (`null`) | Initial reasoning effort for untouched drafts. Explicit user choices (including automatic) take precedence |
80
94
  | `conversationCreatorFilter` | `'all' \| 'mine'` | — | Controlled sidebar creator filter; use with `v-model:conversation-creator-filter` when the host changes it at runtime |
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "4.4.0-rc.6",
4
+ "version": "4.4.0-rc.7",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -445,6 +445,7 @@ const module$1 = defineNuxtModule({
445
445
  const rc = nuxt.options.runtimeConfig;
446
446
  rc.chatApiUrl = options.chatApiUrl || rc.chatApiUrl || process.env.CHAT_API_URL || "";
447
447
  rc.public = rc.public || {};
448
+ rc.public.chatConfig = { ...rc.public.chatConfig };
448
449
  const existingChatNuxtRuntimeConfig = rc.chatNuxt;
449
450
  rc.chatNuxt = {
450
451
  chatApiUrl: existingChatNuxtRuntimeConfig?.chatApiUrl || rc.chatApiUrl,
@@ -1,11 +1,8 @@
1
- import { useChatAuth } from '#chat-auth';
2
- declare var __VLS_13: {}, __VLS_16: {
3
- message: useChatAuth;
4
- };
1
+ declare var __VLS_26: {}, __VLS_29: any;
5
2
  type __VLS_Slots = {} & {
6
- 'sidebar-bottom'?: (props: typeof __VLS_13) => any;
3
+ 'sidebar-bottom'?: (props: typeof __VLS_26) => any;
7
4
  } & {
8
- 'failed-message-actions'?: (props: typeof __VLS_16) => any;
5
+ 'failed-message-actions'?: (props: typeof __VLS_29) => any;
9
6
  };
10
7
  declare const __VLS_base: import("vue").DefineSetupFnComponent<Record<string, any>, {}, {}, Record<string, any> & {}, import("vue").PublicProps>;
11
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -8,9 +8,9 @@ import { provideChatRequestContext } from "../../shared/composables/chat-request
8
8
  import { provideEmbedConfig, useEmbedConfig } from "../composables/embed-config";
9
9
  import { normalizeConversationScope, normalizeTelaAgentId, resolveConversationScopeKeySegment } from "../../conversations/scope";
10
10
  import ChatEmbedInner from "./chat-embed-inner.vue";
11
+ import { useAgentHarness } from "../composables/agent-harness";
11
12
  const props = defineProps({
12
13
  syncBrowserState: { type: Boolean, required: false, default: true },
13
- chatConfig: { type: Object, required: false },
14
14
  telaAgentBranch: { type: [String, null], required: false },
15
15
  conversationId: { type: [String, null], required: false },
16
16
  conversationEvents: { type: [Array, null], required: false },
@@ -47,11 +47,30 @@ const props = defineProps({
47
47
  launcherLabel: { type: String, required: false }
48
48
  });
49
49
  const emit = defineEmits(["update:conversationId", "update:conversationTitle", "update:conversationCreatorFilter", "action", "messageFeedback", "messageTerminal"]);
50
+ const LightweightAdapter = defineAsyncComponent(() => import("../../lightweight/embed/components/chat-embed-adapter.vue"));
50
51
  const parentEmbedConfig = useEmbedConfig();
51
52
  const { activeOrganization } = useChatAuth();
52
53
  const workspaceId = computed(() => activeOrganization.value?.id?.trim() ?? "");
53
54
  const normalizedTelaAgentId = computed(() => normalizeTelaAgentId(props.telaAgentId));
54
55
  const telaAgentBranch = computed(() => props.telaAgentBranch?.trim() || null);
56
+ const selectedConversationId = ref(props.conversationId ?? props.initialConversationId ?? null);
57
+ watch(() => props.conversationId, (value) => {
58
+ selectedConversationId.value = value ?? null;
59
+ });
60
+ function selectConversation(value) {
61
+ selectedConversationId.value = value;
62
+ emit("update:conversationId", value);
63
+ }
64
+ const { harness, loading: harnessLoading, error: harnessError, retry: retryHarness } = useAgentHarness({
65
+ workspaceId,
66
+ agentId: normalizedTelaAgentId,
67
+ branch: telaAgentBranch,
68
+ conversationId: selectedConversationId,
69
+ onConversationCleared: () => emit("update:conversationId", null),
70
+ conversationScope: computed(() => normalizeConversationScope(props.conversationScope))
71
+ });
72
+ const harnessComponents = { claude: ChatEmbedInner, lightweight: LightweightAdapter };
73
+ const harnessComponent = computed(() => harness.value ? harnessComponents[harness.value] : null);
55
74
  const normalizedConversationScope = computed(() => normalizeConversationScope(props.conversationScope));
56
75
  const hideSettings = computed(() => props.hideSettings ?? false);
57
76
  const ignoredPropsWarningKey = ref(null);
@@ -83,6 +102,7 @@ provideEmbedConfig({
83
102
  messageFeedback: computed(() => props.messageFeedback)
84
103
  }, { syncGlobal: !parentEmbedConfig });
85
104
  provideChatRequestContext({
105
+ harness,
86
106
  workspaceId,
87
107
  telaAgentId: normalizedTelaAgentId,
88
108
  telaAgentBranch,
@@ -119,14 +139,26 @@ provideChatMessageTerminal((payload) => {
119
139
  </script>
120
140
 
121
141
  <template>
122
- <ChatEmbedInner
123
- v-if="workspaceId"
124
- :key="`${workspaceId}:${normalizedTelaAgentId ?? 'default-chat'}:branch:${telaAgentBranch ?? ''}:scope:${resolveConversationScopeKeySegment(normalizedConversationScope)}`"
142
+ <TelaChatEmptyState
143
+ v-if="workspaceId && harnessError"
144
+ compact
145
+ role="alert"
146
+ title="Não foi possível carregar o agente"
147
+ :description="harnessError"
148
+ >
149
+ <TelaButton variant="secondary" @click="retryHarness">
150
+ Tentar novamente
151
+ </TelaButton>
152
+ </TelaChatEmptyState>
153
+ <component
154
+ :is="harnessComponent"
155
+ v-if="workspaceId && harnessComponent"
156
+ :key="`${harness}:${workspaceId}:${normalizedTelaAgentId ?? 'default-chat'}:branch:${telaAgentBranch ?? ''}:scope:${resolveConversationScopeKeySegment(normalizedConversationScope)}`"
125
157
  :sync-browser-state="syncBrowserState"
126
- :conversation-id="conversationId"
158
+ :conversation-id="selectedConversationId"
127
159
  :conversation-events="conversationEvents"
128
- :composer-disabled="composerDisabled"
129
- :initial-conversation-id="initialConversationId"
160
+ :composer-disabled="composerDisabled || harnessLoading || !!harnessError"
161
+ :initial-conversation-id="selectedConversationId"
130
162
  :hide-sidebar="hideSidebar"
131
163
  :sidebar="sidebar"
132
164
  :hide-settings="hideSettings"
@@ -134,7 +166,7 @@ provideChatMessageTerminal((payload) => {
134
166
  :suggestions="suggestions"
135
167
  :user="normalizedTelaAgentId ? null : user"
136
168
  :features="features"
137
- @update:conversation-id="$emit('update:conversationId', $event)"
169
+ @update:conversation-id="selectConversation"
138
170
  @update:conversation-title="$emit('update:conversationTitle', $event)"
139
171
  @update:conversation-creator-filter="$emit('update:conversationCreatorFilter', $event)"
140
172
  >
@@ -144,5 +176,8 @@ provideChatMessageTerminal((payload) => {
144
176
  <template v-if="$slots['failed-message-actions']" #failed-message-actions="slotProps">
145
177
  <slot name="failed-message-actions" v-bind="slotProps" />
146
178
  </template>
147
- </ChatEmbedInner>
179
+ </component>
180
+ <TelaChatEmptyState v-else-if="workspaceId && harnessLoading" compact role="status" aria-live="polite">
181
+ <TelaChatTextShimmer>Carregando agente…</TelaChatTextShimmer>
182
+ </TelaChatEmptyState>
148
183
  </template>
@@ -1,11 +1,8 @@
1
- import { useChatAuth } from '#chat-auth';
2
- declare var __VLS_13: {}, __VLS_16: {
3
- message: useChatAuth;
4
- };
1
+ declare var __VLS_26: {}, __VLS_29: any;
5
2
  type __VLS_Slots = {} & {
6
- 'sidebar-bottom'?: (props: typeof __VLS_13) => any;
3
+ 'sidebar-bottom'?: (props: typeof __VLS_26) => any;
7
4
  } & {
8
- 'failed-message-actions'?: (props: typeof __VLS_16) => any;
5
+ 'failed-message-actions'?: (props: typeof __VLS_29) => any;
9
6
  };
10
7
  declare const __VLS_base: import("vue").DefineSetupFnComponent<Record<string, any>, {}, {}, Record<string, any> & {}, import("vue").PublicProps>;
11
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -3,7 +3,6 @@ import { computed, ref, watch } from "vue";
3
3
  import { provideConversationCreatorFilterState } from "../../conversations/composables/conversation-creator-filter-state";
4
4
  const { hideSidebar = void 0, syncBrowserState = true, closeOnOutsideClick = true, ...props } = defineProps({
5
5
  syncBrowserState: { type: Boolean, required: false },
6
- chatConfig: { type: Object, required: false },
7
6
  telaAgentBranch: { type: [String, null], required: false },
8
7
  conversationId: { type: [String, null], required: false },
9
8
  conversationEvents: { type: [Array, null], required: false },
@@ -0,0 +1,15 @@
1
+ import { type Ref } from 'vue';
2
+ /** Resolve the executable agent before mounting either protocol-specific UI. */
3
+ export declare function useAgentHarness(input: {
4
+ workspaceId: Ref<string>;
5
+ agentId: Ref<string | null>;
6
+ branch: Ref<string | null>;
7
+ conversationId?: Ref<string | null>;
8
+ conversationScope?: Ref<string | null>;
9
+ onConversationCleared?: () => void;
10
+ }): {
11
+ harness: any;
12
+ loading: Ref<boolean, boolean>;
13
+ error: Ref<string | null, string | null>;
14
+ retry: () => void;
15
+ };
@@ -0,0 +1,55 @@
1
+ import { ref, watch } from "vue";
2
+ export function useAgentHarness(input) {
3
+ const harness = ref(null);
4
+ const loading = ref(false);
5
+ const error = ref(null);
6
+ const retryCount = ref(0);
7
+ watch([input.workspaceId, input.agentId, input.branch, input.conversationScope ?? ref(null)], () => {
8
+ if (input.conversationId?.value) {
9
+ input.conversationId.value = null;
10
+ input.onConversationCleared?.();
11
+ }
12
+ }, { flush: "sync" });
13
+ watch([input.workspaceId, input.agentId, input.branch, input.conversationId ?? ref(null), input.conversationScope ?? ref(null), retryCount], async ([workspaceId, agentId, branch, conversationId, conversationScope], previous, onCleanup) => {
14
+ if (workspaceId !== previous?.[0] || agentId !== previous?.[1] || branch !== previous?.[2] || conversationScope !== previous?.[4]) {
15
+ harness.value = null;
16
+ }
17
+ error.value = null;
18
+ loading.value = false;
19
+ if (!workspaceId)
20
+ return;
21
+ const controller = new AbortController();
22
+ onCleanup(() => controller.abort());
23
+ loading.value = true;
24
+ const headers = { "x-chat-workspace-id": workspaceId };
25
+ if (agentId)
26
+ headers["x-chat-tela-agent-id"] = agentId;
27
+ if (branch)
28
+ headers["x-chat-tela-agent-branch"] = branch;
29
+ if (conversationScope)
30
+ headers["x-chat-conversation-scope"] = conversationScope;
31
+ try {
32
+ const result = await $fetch("/api/chat/agent/runtime", {
33
+ headers,
34
+ ...conversationId ? { query: { conversationId } } : {},
35
+ signal: controller.signal
36
+ });
37
+ if (controller.signal.aborted)
38
+ return;
39
+ if (!result?.agentId || agentId && result.agentId !== agentId || result.harness !== "claude" && result.harness !== "lightweight") {
40
+ throw new Error("Unsupported or missing agent harness");
41
+ }
42
+ harness.value = result.harness;
43
+ } catch {
44
+ if (!controller.signal.aborted) {
45
+ error.value = "N\xE3o foi poss\xEDvel carregar a configura\xE7\xE3o do agente.";
46
+ }
47
+ } finally {
48
+ if (!controller.signal.aborted)
49
+ loading.value = false;
50
+ }
51
+ }, { immediate: true, flush: "sync" });
52
+ return { harness, loading, error, retry: () => {
53
+ retryCount.value++;
54
+ } };
55
+ }
@@ -1,4 +1,3 @@
1
- import type { ChatHarness } from '../types/schemas/chat/chat-config.js';
2
1
  import type { ChatReasoningEffort } from '../types/schemas/chat/models.js';
3
2
  import type { Component, DeepReadonly } from 'vue';
4
3
  import type { ConversationEvent } from '../types/schemas/chat/conversations.js';
@@ -50,10 +49,6 @@ export type ChatSidebarConfig = {
50
49
  export type ChatEmbedSharedProps = {
51
50
  /** Let the embed synchronize its conversation with the browser URL and title. Defaults to true. */
52
51
  syncBrowserState?: boolean;
53
- /** Defaults to Claude. Lightweight uses its own execution and rendering path. */
54
- chatConfig?: {
55
- harness?: ChatHarness;
56
- };
57
52
  /** Optional repository branch for new agent sessions. Omit to use the published version. */
58
53
  telaAgentBranch?: string | null;
59
54
  /** Controlled conversation id. Use with `v-model:conversation-id` when the host owns active conversation state. */
@@ -1,5 +1,5 @@
1
1
  export declare function useLightweightModelSelection(): {
2
2
  ensureReady: () => Promise<void>;
3
- resolve: (selection: string | null) => string | null;
3
+ resolve: (selection: string | null) => any;
4
4
  ensureSelectedModel: (current: string | null, selected: string | null, persist: (value: string | null) => Promise<boolean>) => Promise<void>;
5
5
  };
@@ -1,6 +1,6 @@
1
1
  import { useEmbedConfig } from "../../../embed/composables/embed-config.js";
2
2
  import { useChatModelCatalog } from "../../messages/composables/chat-model-catalog.js";
3
- import { ensureConversationUsesSelectedModel, resolveAvailableChatModels, resolveChatModelSelection } from "../../messages/model-availability.js";
3
+ import { ensureConversationUsesSelectedModel, resolveAvailableChatModels } from "../../../messages/model-availability.js";
4
4
  export function useLightweightModelSelection() {
5
5
  const embedConfig = useEmbedConfig();
6
6
  const { scopeKey, models, defaultModel, isLoaded, load } = useChatModelCatalog();
@@ -14,7 +14,7 @@ export function useLightweightModelSelection() {
14
14
  failed.value = true;
15
15
  });
16
16
  }, { immediate: true });
17
- const available = computed(() => models.value.length ? resolveAvailableChatModels(models.value.map((model) => model.id), embedConfig?.availableModels.value) : []);
17
+ const available = computed(() => models.value.length ? resolveAvailableChatModels(embedConfig?.availableModels.value, models.value.map((model) => model.id)) : []);
18
18
  const fallback = computed(() => available.value.includes(defaultModel.value ?? "") ? defaultModel.value : available.value[0] ?? null);
19
19
  async function ensureReady() {
20
20
  await ready;
@@ -23,7 +23,7 @@ export function useLightweightModelSelection() {
23
23
  }
24
24
  return {
25
25
  ensureReady,
26
- resolve: (selection) => resolveChatModelSelection(selection, fallback.value),
26
+ resolve: (selection) => selection ?? fallback.value,
27
27
  ensureSelectedModel: (current, selected, persist) => ensureConversationUsesSelectedModel(current, selected, persist, available.value)
28
28
  };
29
29
  }
@@ -1,53 +1,34 @@
1
- import type { SendMessageResponse } from '../../../types/schemas/chat/messages.js';
1
+ import { applyLightweightPart } from '../../../types/schemas/integrations/lightweight.js';
2
2
  import type { LightweightTurn } from '../../../types/schemas/integrations/lightweight.js';
3
- export type DirectStreamActivity = 'idle' | 'connected' | 'active';
4
- export type DirectStreamResultStatus = 'completed' | 'waiting_messages';
5
- export type DirectStreamResult = {
6
- status: DirectStreamResultStatus;
7
- text: string;
8
- };
9
- export type SseFrame = {
10
- event?: string;
11
- data: string;
12
- };
13
- export type MessagePartsEvent = {
14
- kind: 'message-parts';
15
- harness?: string;
16
- sessionId?: string;
17
- timestamp?: number;
18
- runId: string;
19
- deltaSeq: number;
20
- parentToolUseId?: string | null;
21
- event: Record<string, unknown>;
22
- };
23
- export type MessagePartsAccumulator = {
3
+ import type { DirectStreamOptions, MessagePartsEvent } from '../../../messages/composables/use-direct-stream.js';
4
+ type MessagePartsAccumulator = {
24
5
  runId: string | null;
25
6
  lightweight?: LightweightTurn;
26
7
  };
27
- export declare function parseSseFrames(source: string): {
28
- frames: SseFrame[];
29
- rest: string;
30
- };
31
8
  export declare function createMessagePartsAccumulator(runId?: string | null): MessagePartsAccumulator;
32
9
  export declare function applyMessagePartsEvent(accumulator: MessagePartsAccumulator, input: MessagePartsEvent): {
33
- text: string;
34
- thinking: string;
10
+ text: any;
11
+ thinking: any;
35
12
  changed: boolean;
13
+ reasoning?: undefined;
14
+ files?: undefined;
15
+ } | {
16
+ text: any;
17
+ thinking: any;
18
+ changed: any;
19
+ reasoning: any;
20
+ files: any;
36
21
  };
37
- export declare function useDirectStream(options?: {
38
- onOptimisticResult?: (result: DirectStreamResult) => void;
39
- onResult?: (result: DirectStreamResult) => void;
40
- onTerminal?: () => void;
41
- onDisconnect?: () => void;
42
- }): {
22
+ export declare function useDirectStream(options?: DirectStreamOptions): {
43
23
  activity: any;
44
24
  streamingText: any;
45
25
  streamingThinking: any;
46
26
  activeRunId: any;
47
27
  streamingReasoning: any;
48
28
  streamingFiles: any;
49
- connectMessage: (url: string, init: RequestInit, onAccepted: (response: SendMessageResponse) => void) => Promise<SendMessageResponse>;
29
+ connectMessage: (url: string, init: RequestInit, onAccepted: (response: applyLightweightPart) => void) => Promise<SendMessageResponse>;
50
30
  connectConversation: (url: string, init: RequestInit, expectedAssistantMessageId?: string) => Promise<void>;
51
31
  beginMessage: () => void;
52
32
  disconnect: (reset?: boolean) => void;
53
33
  };
34
+ export {};
@@ -1,36 +1,5 @@
1
1
  import { applyLightweightPart, createLightweightTurn } from "../../../types/schemas/integrations/lightweight.js";
2
- import { DirectStreamHttpError, getChatApiErrorCode } from "../../../shared/chat-api-error.js";
3
- function isRecord(value) {
4
- return value !== null && typeof value === "object" && !Array.isArray(value);
5
- }
6
- export function parseSseFrames(source) {
7
- const frames = [];
8
- let rest = source;
9
- while (true) {
10
- const boundary = /\r?\n\r?\n/.exec(rest);
11
- if (!boundary || boundary.index === void 0)
12
- break;
13
- const rawFrame = rest.slice(0, boundary.index);
14
- rest = rest.slice(boundary.index + boundary[0].length);
15
- let event;
16
- const data = [];
17
- for (const line of rawFrame.split(/\r?\n/)) {
18
- if (line.startsWith(":"))
19
- continue;
20
- const separator = line.indexOf(":");
21
- const field = separator === -1 ? line : line.slice(0, separator);
22
- const rawValue = separator === -1 ? "" : line.slice(separator + 1);
23
- const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
24
- if (field === "event")
25
- event = value;
26
- else if (field === "data")
27
- data.push(value);
28
- }
29
- if (data.length > 0)
30
- frames.push({ ...event ? { event } : {}, data: data.join("\n") });
31
- }
32
- return { frames, rest };
33
- }
2
+ import { useDirectStreamWithProtocol } from "../../../messages/composables/use-direct-stream.js";
34
3
  export function createMessagePartsAccumulator(runId = null) {
35
4
  return {
36
5
  runId
@@ -43,251 +12,11 @@ export function applyMessagePartsEvent(accumulator, input) {
43
12
  accumulator.lightweight = createLightweightTurn(input.runId, input.sessionId ?? "");
44
13
  accumulator.runId = input.runId;
45
14
  const changed = applyLightweightPart(accumulator.lightweight, input.event, input.deltaSeq, input.timestamp ?? 0);
46
- return { text: accumulator.lightweight.text, thinking: accumulator.lightweight.thinking, changed };
47
- }
48
- function reviveMessagePair(input) {
49
- return {
50
- userMessage: {
51
- ...input.userMessage,
52
- createdAt: new Date(input.userMessage.createdAt),
53
- updatedAt: input.userMessage.updatedAt ? new Date(input.userMessage.updatedAt) : null
54
- },
55
- assistantMessage: {
56
- ...input.assistantMessage,
57
- createdAt: new Date(input.assistantMessage.createdAt),
58
- updatedAt: input.assistantMessage.updatedAt ? new Date(input.assistantMessage.updatedAt) : null
59
- }
60
- };
15
+ return { text: accumulator.lightweight.text, thinking: accumulator.lightweight.thinking, changed, reasoning: accumulator.lightweight.reasoning, files: accumulator.lightweight.files };
61
16
  }
62
17
  export function useDirectStream(options = {}) {
63
- const activity = ref("idle");
64
- const streamingText = ref(null);
65
- const streamingThinking = ref("");
66
- const activeRunId = ref(null);
67
- const streamingReasoning = ref([]);
68
- const streamingFiles = ref([]);
69
- let accumulator = createMessagePartsAccumulator();
70
- let abortController = null;
71
- function resetAccumulator(runId = null) {
72
- accumulator = createMessagePartsAccumulator(runId);
73
- activeRunId.value = runId;
74
- streamingText.value = null;
75
- streamingThinking.value = "";
76
- streamingReasoning.value = [];
77
- streamingFiles.value = [];
78
- }
79
- function beginMessage() {
80
- resetAccumulator(activeRunId.value);
81
- activity.value = "idle";
82
- }
83
- function disconnect(reset = true) {
84
- abortController?.abort();
85
- abortController = null;
86
- activity.value = "idle";
87
- if (reset)
88
- resetAccumulator();
89
- }
90
- async function connectStream(url, init, onAccepted, expectedAssistantMessageId) {
91
- if (import.meta.server)
92
- throw new Error("Message streaming is unavailable during server rendering");
93
- abortController?.abort();
94
- const controller = new AbortController();
95
- abortController = controller;
96
- let response;
97
- try {
98
- response = await fetch(url, { ...init, signal: controller.signal });
99
- } catch (error) {
100
- if (abortController === controller)
101
- abortController = null;
102
- throw error;
103
- }
104
- if (!response.ok) {
105
- if (abortController === controller)
106
- abortController = null;
107
- const text = await response.text().catch(() => "");
108
- let body = text;
109
- if (text) {
110
- try {
111
- body = JSON.parse(text);
112
- } catch {
113
- body = { error: text };
114
- }
115
- }
116
- throw new DirectStreamHttpError(response.status, body, getChatApiErrorCode(body));
117
- }
118
- if (!response.body) {
119
- if (abortController === controller)
120
- abortController = null;
121
- throw new Error("Message stream response has no body");
122
- }
123
- if (controller.signal.aborted)
124
- throw new DOMException("Aborted", "AbortError");
125
- activity.value = "connected";
126
- let resolveAccepted;
127
- let rejectAccepted;
128
- const acceptedPromise = new Promise((resolve, reject) => {
129
- resolveAccepted = resolve;
130
- rejectAccepted = reject;
131
- });
132
- let accepted = !onAccepted;
133
- if (accepted)
134
- resolveAccepted();
135
- let optimisticResultReported = false;
136
- let terminalEventObserved = false;
137
- let terminalReported = false;
138
- let recoveryRequested = false;
139
- function requestRecovery() {
140
- if (recoveryRequested)
141
- return;
142
- recoveryRequested = true;
143
- options.onDisconnect?.();
144
- }
145
- function reportTerminal() {
146
- if (terminalReported)
147
- return;
148
- terminalReported = true;
149
- recoveryRequested = true;
150
- options.onTerminal?.();
151
- }
152
- function reportOptimisticResult(status) {
153
- if (status === "completed" || status === "waiting_messages") {
154
- const result = {
155
- status,
156
- text: streamingText.value ?? ""
157
- };
158
- if (!optimisticResultReported) {
159
- optimisticResultReported = true;
160
- options.onOptimisticResult?.(result);
161
- }
162
- return result;
163
- }
164
- return null;
165
- }
166
- function settleResult(status) {
167
- if (terminalEventObserved)
168
- return;
169
- terminalEventObserved = true;
170
- const result = reportOptimisticResult(status);
171
- if (result)
172
- options.onResult?.(result);
173
- reportTerminal();
174
- }
175
- function settleTerminal() {
176
- if (terminalEventObserved)
177
- return;
178
- terminalEventObserved = true;
179
- reportTerminal();
180
- }
181
- void (async () => {
182
- try {
183
- const reader = response.body.getReader();
184
- const decoder = new TextDecoder();
185
- let buffered = "";
186
- while (!controller.signal.aborted) {
187
- const { value, done } = await reader.read();
188
- if (done)
189
- break;
190
- if (controller.signal.aborted)
191
- break;
192
- buffered += decoder.decode(value, { stream: true });
193
- const parsed = parseSseFrames(buffered);
194
- buffered = parsed.rest;
195
- for (const frame of parsed.frames) {
196
- if (controller.signal.aborted)
197
- break;
198
- if (frame.data === "[DONE]")
199
- continue;
200
- if (frame.event === "message-accepted") {
201
- if (!onAccepted)
202
- continue;
203
- try {
204
- const messagePair = reviveMessagePair(JSON.parse(frame.data));
205
- accepted = true;
206
- onAccepted(messagePair);
207
- resolveAccepted(messagePair);
208
- } catch (error) {
209
- rejectAccepted(error);
210
- }
211
- continue;
212
- }
213
- let event;
214
- try {
215
- event = JSON.parse(frame.data);
216
- } catch {
217
- continue;
218
- }
219
- if (!isRecord(event) || typeof event.kind !== "string")
220
- continue;
221
- if (event.kind === "conversation-state" && expectedAssistantMessageId && event.assistantMessageId !== expectedAssistantMessageId) {
222
- controller.abort();
223
- requestRecovery();
224
- break;
225
- }
226
- activity.value = "active";
227
- if (event.kind === "result") {
228
- settleResult(event.status);
229
- continue;
230
- }
231
- if (event.kind === "steps" && (event.status === "completed" || event.status === "waiting_messages")) {
232
- reportOptimisticResult(event.status);
233
- reportTerminal();
234
- continue;
235
- }
236
- if (event.kind === "error") {
237
- if (event.recoverable === true) {
238
- requestRecovery();
239
- } else {
240
- settleTerminal();
241
- }
242
- continue;
243
- }
244
- if (event.kind === "status" && (event.status === "failed" || event.status === "cancelled")) {
245
- settleTerminal();
246
- continue;
247
- }
248
- if (event.kind !== "message-parts" || typeof event.runId !== "string" || typeof event.deltaSeq !== "number" || !Number.isInteger(event.deltaSeq) || event.deltaSeq < 0 || !isRecord(event.event)) {
249
- continue;
250
- }
251
- const current = applyMessagePartsEvent(accumulator, event);
252
- activeRunId.value = accumulator.runId;
253
- if (current.changed) {
254
- streamingText.value = current.text;
255
- streamingThinking.value = current.thinking;
256
- if (accumulator.lightweight) {
257
- streamingReasoning.value = [...accumulator.lightweight.reasoning];
258
- streamingFiles.value = [...accumulator.lightweight.files];
259
- }
260
- }
261
- }
262
- }
263
- if (!accepted)
264
- rejectAccepted(new Error("Message stream ended before acknowledgement"));
265
- } catch (error) {
266
- if (!accepted)
267
- rejectAccepted(error);
268
- } finally {
269
- if (abortController === controller) {
270
- abortController = null;
271
- if (!onAccepted)
272
- activity.value = "idle";
273
- }
274
- if (accepted && !terminalEventObserved && !controller.signal.aborted)
275
- requestRecovery();
276
- }
277
- })();
278
- return acceptedPromise;
279
- }
280
- onUnmounted(() => disconnect());
281
- return {
282
- activity: readonly(activity),
283
- streamingText: readonly(streamingText),
284
- streamingThinking: readonly(streamingThinking),
285
- activeRunId: readonly(activeRunId),
286
- streamingReasoning: readonly(streamingReasoning),
287
- streamingFiles: readonly(streamingFiles),
288
- connectMessage: (url, init, onAccepted) => connectStream(url, init, onAccepted),
289
- connectConversation: (url, init, expectedAssistantMessageId) => connectStream(url, { ...init, method: "GET" }, void 0, expectedAssistantMessageId),
290
- beginMessage,
291
- disconnect
292
- };
18
+ return useDirectStreamWithProtocol({
19
+ create: createMessagePartsAccumulator,
20
+ apply: applyMessagePartsEvent
21
+ }, options);
293
22
  }
@@ -1,10 +1,10 @@
1
- import { resolveAvailableChatModels } from "./model-availability.js";
1
+ import { resolveAvailableChatModels } from "../../messages/model-availability.js";
2
2
  export function buildChatModelOptions(input) {
3
3
  if (input.models.length === 0)
4
4
  return [];
5
5
  const availableModels = resolveAvailableChatModels(
6
- input.models.map((candidate) => candidate.id),
7
- input.availableModels
6
+ input.availableModels,
7
+ input.models.map((candidate) => candidate.id)
8
8
  );
9
9
  const options = availableModels.map((modelId) => {
10
10
  const model = input.models.find((candidate) => candidate.id === modelId);
@@ -1,3 +1,5 @@
1
+ import type { ReasoningItem } from '../../types/schemas/integrations/agent-api.js';
2
+ import type { MessageFile } from '../../types/schemas/chat/files.js';
1
3
  import type { SendMessageResponse } from '../../types/schemas/chat/messages.js';
2
4
  export type DirectStreamActivity = 'idle' | 'connected' | 'active';
3
5
  export type DirectStreamResultStatus = 'completed' | 'waiting_messages';
@@ -11,6 +13,9 @@ export type SseFrame = {
11
13
  };
12
14
  export type MessagePartsEvent = {
13
15
  kind: 'message-parts';
16
+ harness?: string;
17
+ sessionId?: string;
18
+ timestamp?: number;
14
19
  runId: string;
15
20
  deltaSeq: number;
16
21
  parentToolUseId?: string | null;
@@ -32,18 +37,46 @@ export declare function applyMessagePartsEvent(accumulator: MessagePartsAccumula
32
37
  thinking: string;
33
38
  changed: boolean;
34
39
  };
35
- export declare function useDirectStream(options?: {
40
+ export type DirectStreamOptions = {
36
41
  onOptimisticResult?: (result: DirectStreamResult) => void;
37
42
  onResult?: (result: DirectStreamResult) => void;
38
43
  onTerminal?: () => void;
39
44
  onDisconnect?: () => void;
40
- }): {
45
+ };
46
+ type MessagePartsUpdate = {
47
+ text: string;
48
+ thinking: string;
49
+ changed: boolean;
50
+ reasoning?: ReasoningItem[];
51
+ files?: MessageFile[];
52
+ };
53
+ export declare function useDirectStream(options?: DirectStreamOptions): {
54
+ activity: any;
55
+ streamingText: any;
56
+ streamingThinking: any;
57
+ activeRunId: any;
58
+ streamingReasoning: any;
59
+ streamingFiles: any;
60
+ connectMessage: (url: string, init: RequestInit, onAccepted: (response: SendMessageResponse) => void) => Promise<SendMessageResponse>;
61
+ connectConversation: (url: string, init: RequestInit, expectedAssistantMessageId?: string) => Promise<void>;
62
+ beginMessage: () => void;
63
+ disconnect: (reset?: boolean) => void;
64
+ };
65
+ export declare function useDirectStreamWithProtocol<T extends {
66
+ runId: string | null;
67
+ }>(protocol: {
68
+ create: (runId?: string | null) => T;
69
+ apply: (accumulator: T, event: MessagePartsEvent) => MessagePartsUpdate;
70
+ }, options?: DirectStreamOptions): {
41
71
  activity: any;
42
72
  streamingText: any;
43
73
  streamingThinking: any;
44
74
  activeRunId: any;
75
+ streamingReasoning: any;
76
+ streamingFiles: any;
45
77
  connectMessage: (url: string, init: RequestInit, onAccepted: (response: SendMessageResponse) => void) => Promise<SendMessageResponse>;
46
78
  connectConversation: (url: string, init: RequestInit, expectedAssistantMessageId?: string) => Promise<void>;
47
79
  beginMessage: () => void;
48
80
  disconnect: (reset?: boolean) => void;
49
81
  };
82
+ export {};
@@ -114,17 +114,27 @@ function reviveMessagePair(input) {
114
114
  };
115
115
  }
116
116
  export function useDirectStream(options = {}) {
117
+ return useDirectStreamWithProtocol({
118
+ create: createMessagePartsAccumulator,
119
+ apply: applyMessagePartsEvent
120
+ }, options);
121
+ }
122
+ export function useDirectStreamWithProtocol(protocol, options = {}) {
117
123
  const activity = ref("idle");
118
124
  const streamingText = ref(null);
119
125
  const streamingThinking = ref("");
120
126
  const activeRunId = ref(null);
121
- let accumulator = createMessagePartsAccumulator();
127
+ const streamingReasoning = ref([]);
128
+ const streamingFiles = ref([]);
129
+ let accumulator = protocol.create();
122
130
  let abortController = null;
123
131
  function resetAccumulator(runId = null) {
124
- accumulator = createMessagePartsAccumulator(runId);
132
+ accumulator = protocol.create(runId);
125
133
  activeRunId.value = runId;
126
134
  streamingText.value = null;
127
135
  streamingThinking.value = "";
136
+ streamingReasoning.value = [];
137
+ streamingFiles.value = [];
128
138
  }
129
139
  function beginMessage() {
130
140
  resetAccumulator(activeRunId.value);
@@ -298,11 +308,13 @@ export function useDirectStream(options = {}) {
298
308
  if (event.kind !== "message-parts" || typeof event.runId !== "string" || typeof event.deltaSeq !== "number" || !Number.isInteger(event.deltaSeq) || event.deltaSeq < 0 || !isRecord(event.event)) {
299
309
  continue;
300
310
  }
301
- const current = applyMessagePartsEvent(accumulator, event);
311
+ const current = protocol.apply(accumulator, event);
302
312
  activeRunId.value = accumulator.runId;
303
313
  if (current.changed) {
304
314
  streamingText.value = current.text;
305
315
  streamingThinking.value = current.thinking;
316
+ streamingReasoning.value = [...current.reasoning ?? []];
317
+ streamingFiles.value = [...current.files ?? []];
306
318
  }
307
319
  }
308
320
  }
@@ -329,6 +341,8 @@ export function useDirectStream(options = {}) {
329
341
  streamingText: readonly(streamingText),
330
342
  streamingThinking: readonly(streamingThinking),
331
343
  activeRunId: readonly(activeRunId),
344
+ streamingReasoning: readonly(streamingReasoning),
345
+ streamingFiles: readonly(streamingFiles),
332
346
  connectMessage: (url, init, onAccepted) => connectStream(url, init, onAccepted),
333
347
  connectConversation: (url, init, expectedAssistantMessageId) => connectStream(url, { ...init, method: "GET" }, void 0, expectedAssistantMessageId),
334
348
  beginMessage,
@@ -1,6 +1,8 @@
1
1
  import type { ChatModel } from '../types/schemas/chat/models.js';
2
2
  export type AvailableChatModels = readonly [ChatModel, ...ChatModel[]];
3
3
  export declare const MODEL_UPDATE_FAILED_MESSAGE = "N\u00E3o foi poss\u00EDvel atualizar o modelo da conversa";
4
+ export declare const MODEL_UNAVAILABLE_MESSAGE = "O modelo selecionado n\u00E3o est\u00E1 dispon\u00EDvel neste workspace";
4
5
  export declare function resolveAvailableChatModels(availableModels?: readonly string[]): readonly ChatModel[];
6
+ export declare function resolveAvailableChatModels(availableModels: readonly string[] | undefined, catalogModels: readonly string[]): readonly string[];
5
7
  export declare function resolveChatModelSelection(selectedModel: string | null, availableModels?: readonly string[]): string | null;
6
- export declare function ensureConversationUsesSelectedModel(conversationModel: string | null, selectedModel: string | null, updateModel: (model: string | null) => Promise<boolean>): Promise<void>;
8
+ export declare function ensureConversationUsesSelectedModel(conversationModel: string | null, selectedModel: string | null, updateModel: (model: string | null) => Promise<boolean>, availableModels?: readonly string[]): Promise<void>;
@@ -1,14 +1,19 @@
1
- import { chatModelIds, isValidChatModel } from "../types/schemas/chat/models.js";
1
+ import { chatModelIds } from "../types/schemas/chat/models.js";
2
2
  export const MODEL_UPDATE_FAILED_MESSAGE = "N\xE3o foi poss\xEDvel atualizar o modelo da conversa";
3
- export function resolveAvailableChatModels(availableModels) {
3
+ export const MODEL_UNAVAILABLE_MESSAGE = "O modelo selecionado n\xE3o est\xE1 dispon\xEDvel neste workspace";
4
+ export function resolveAvailableChatModels(availableModels, catalogModels) {
5
+ const catalog = catalogModels ?? chatModelIds;
6
+ if (catalog.length === 0)
7
+ throw new Error("Agent API model catalog must contain at least one model");
4
8
  if (availableModels === void 0)
5
- return chatModelIds;
9
+ return [...new Set(catalog)];
6
10
  if (availableModels.length === 0)
7
11
  throw new Error("availableModels must contain at least one model");
8
12
  const uniqueModels = [];
9
13
  for (const model of availableModels) {
10
- if (!isValidChatModel(model))
11
- throw new Error(`${model} is not a supported chat model`);
14
+ if (!catalog.includes(model)) {
15
+ throw new Error(catalogModels ? `${model} is not available in this workspace` : `${model} is not a supported chat model`);
16
+ }
12
17
  if (!uniqueModels.includes(model))
13
18
  uniqueModels.push(model);
14
19
  }
@@ -20,7 +25,9 @@ export function resolveChatModelSelection(selectedModel, availableModels) {
20
25
  const resolvedModels = resolveAvailableChatModels(availableModels);
21
26
  return selectedModel && resolvedModels.includes(selectedModel) ? selectedModel : resolvedModels[0];
22
27
  }
23
- export async function ensureConversationUsesSelectedModel(conversationModel, selectedModel, updateModel) {
28
+ export async function ensureConversationUsesSelectedModel(conversationModel, selectedModel, updateModel, availableModels) {
29
+ if (selectedModel && availableModels && !availableModels.includes(selectedModel))
30
+ throw new Error(MODEL_UNAVAILABLE_MESSAGE);
24
31
  if (conversationModel === selectedModel)
25
32
  return;
26
33
  if (!await updateModel(selectedModel))
@@ -1,14 +1,14 @@
1
1
  import type { ChatHarness } from '../../types/schemas/chat/chat-config.js';
2
2
  import type { MaybeRefOrGetter, Ref } from 'vue';
3
3
  export type ChatRequestContext = {
4
- harness?: Ref<ChatHarness>;
4
+ harness?: Ref<ChatHarness | null>;
5
5
  workspaceId: Ref<string>;
6
6
  telaAgentId: Ref<string | null>;
7
7
  telaAgentBranch: Ref<string | null>;
8
8
  conversationScope: Ref<string | null>;
9
9
  };
10
10
  type ChatRequestContextInput = {
11
- harness?: MaybeRefOrGetter<ChatHarness>;
11
+ harness?: MaybeRefOrGetter<ChatHarness | null>;
12
12
  workspaceId: MaybeRefOrGetter<string | null | undefined>;
13
13
  telaAgentId?: MaybeRefOrGetter<string | null | undefined>;
14
14
  telaAgentBranch?: MaybeRefOrGetter<string | null | undefined>;
@@ -5,7 +5,7 @@ function normalizeOptionalHeaderValue(value) {
5
5
  }
6
6
  function useGlobalChatRequestContextState() {
7
7
  return {
8
- harness: useState("chat-request-context-harness", () => "claude"),
8
+ harness: useState("chat-request-context-harness", () => null),
9
9
  provided: useState("chat-request-context-provided", () => false),
10
10
  workspaceId: useState("chat-request-context-workspace-id", () => ""),
11
11
  telaAgentId: useState("chat-request-context-tela-agent-id", () => null),
@@ -15,7 +15,7 @@ function useGlobalChatRequestContextState() {
15
15
  }
16
16
  export function provideChatRequestContext(input, options = {}) {
17
17
  const context = {
18
- harness: computed(() => toValue(input.harness) ?? "claude"),
18
+ harness: computed(() => toValue(input.harness) ?? null),
19
19
  workspaceId: computed(() => normalizeOptionalHeaderValue(toValue(input.workspaceId)) ?? ""),
20
20
  telaAgentId: computed(() => normalizeOptionalHeaderValue(toValue(input.telaAgentId))),
21
21
  telaAgentBranch: computed(() => normalizeOptionalHeaderValue(toValue(input.telaAgentBranch))),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "4.4.0-rc.6",
3
+ "version": "4.4.0-rc.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -1,7 +0,0 @@
1
- import type { ChatModel } from '../../types/schemas/chat/lightweight-models.js';
2
- export type AvailableChatModels = readonly [ChatModel, ...ChatModel[]];
3
- export declare const MODEL_UPDATE_FAILED_MESSAGE = "N\u00E3o foi poss\u00EDvel atualizar o modelo da conversa";
4
- export declare const MODEL_UNAVAILABLE_MESSAGE = "O modelo selecionado n\u00E3o est\u00E1 dispon\u00EDvel neste workspace";
5
- export declare function resolveAvailableChatModels(catalogModels: readonly ChatModel[], availableModels?: readonly ChatModel[]): readonly ChatModel[];
6
- export declare function resolveChatModelSelection(selectedModel: string | null, defaultModel: string | null): string | null;
7
- export declare function ensureConversationUsesSelectedModel(conversationModel: string | null, selectedModel: string | null, updateModel: (model: string | null) => Promise<boolean>, availableModels?: readonly string[]): Promise<void>;
@@ -1,29 +0,0 @@
1
- export const MODEL_UPDATE_FAILED_MESSAGE = "N\xE3o foi poss\xEDvel atualizar o modelo da conversa";
2
- export const MODEL_UNAVAILABLE_MESSAGE = "O modelo selecionado n\xE3o est\xE1 dispon\xEDvel neste workspace";
3
- export function resolveAvailableChatModels(catalogModels, availableModels) {
4
- if (catalogModels.length === 0)
5
- throw new Error("Agent API model catalog must contain at least one model");
6
- if (availableModels === void 0)
7
- return [...new Set(catalogModels)];
8
- if (availableModels.length === 0)
9
- throw new Error("availableModels must contain at least one model");
10
- const uniqueModels = [];
11
- for (const model of availableModels) {
12
- if (!catalogModels.includes(model))
13
- throw new Error(`${model} is not available in this workspace`);
14
- if (!uniqueModels.includes(model))
15
- uniqueModels.push(model);
16
- }
17
- return uniqueModels;
18
- }
19
- export function resolveChatModelSelection(selectedModel, defaultModel) {
20
- return selectedModel ?? defaultModel;
21
- }
22
- export async function ensureConversationUsesSelectedModel(conversationModel, selectedModel, updateModel, availableModels) {
23
- if (selectedModel && availableModels && !availableModels.includes(selectedModel))
24
- throw new Error(MODEL_UNAVAILABLE_MESSAGE);
25
- if (conversationModel === selectedModel)
26
- return;
27
- if (!await updateModel(selectedModel))
28
- throw new Error(MODEL_UPDATE_FAILED_MESSAGE);
29
- }