lemma-sdk 0.2.20 → 0.2.21

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/README.md CHANGED
@@ -108,7 +108,12 @@ const assistantPayload: CreateAssistantInput = {
108
108
  ## Auth Helpers
109
109
 
110
110
  ```ts
111
- import { LemmaClient, buildAuthUrl, resolveSafeRedirectUri } from "lemma-sdk";
111
+ import {
112
+ LemmaClient,
113
+ buildAuthUrl,
114
+ buildFederatedLogoutUrl,
115
+ resolveSafeRedirectUri,
116
+ } from "lemma-sdk";
112
117
 
113
118
  const client = new LemmaClient({
114
119
  apiUrl: "https://api-next.asur.work",
@@ -131,6 +136,14 @@ await client.auth.signOut();
131
136
  const token = await client.auth.getAccessToken();
132
137
  const refreshed = await client.auth.refreshAccessToken();
133
138
  client.auth.redirectToAuth({ mode: "signup", redirectUri: safeRedirect });
139
+
140
+ // Build upstream logout URL (server/client)
141
+ const federatedLogoutUrl = buildFederatedLogoutUrl(client.authUrl, {
142
+ redirectUri: safeRedirect,
143
+ });
144
+
145
+ // Browser: sign out locally, then clear upstream SSO and return to app
146
+ await client.auth.redirectToFederatedLogout({ redirectUri: safeRedirect });
134
147
  ```
135
148
 
136
149
  ### Browser Testing With Injected Token
package/dist/auth.d.ts CHANGED
@@ -28,6 +28,7 @@ export interface AuthState {
28
28
  }
29
29
  export type AuthListener = (state: AuthState) => void;
30
30
  export type AuthRedirectMode = "login" | "signup";
31
+ type AuthQueryParams = Record<string, string | number | boolean | Array<string | number | boolean> | null | undefined>;
31
32
  export interface BuildAuthUrlOptions {
32
33
  /** Optional auth path segment relative to authUrl pathname, e.g. "callback" -> /auth/callback. */
33
34
  path?: string;
@@ -36,7 +37,35 @@ export interface BuildAuthUrlOptions {
36
37
  /** Redirect URI passed to auth service. */
37
38
  redirectUri?: string;
38
39
  /** Additional query parameters appended to auth URL. */
39
- params?: Record<string, string | number | boolean | Array<string | number | boolean> | null | undefined>;
40
+ params?: AuthQueryParams;
41
+ }
42
+ export interface BuildFederatedLogoutUrlOptions {
43
+ /**
44
+ * Optional auth path segment for logout, relative to authUrl pathname.
45
+ * Defaults to "logout" (for example: https://auth.example.com/auth/logout).
46
+ */
47
+ path?: string;
48
+ /**
49
+ * Post-logout redirect URI passed to the auth service.
50
+ */
51
+ redirectUri?: string;
52
+ /**
53
+ * Query parameter name used for redirect URI. Defaults to "redirect_uri".
54
+ */
55
+ redirectParam?: string;
56
+ /** Additional query parameters appended to logout URL. */
57
+ params?: AuthQueryParams;
58
+ }
59
+ export interface RedirectToFederatedLogoutOptions extends Omit<BuildFederatedLogoutUrlOptions, "redirectUri"> {
60
+ /**
61
+ * Post-logout redirect URI. Defaults to current location.
62
+ */
63
+ redirectUri?: string;
64
+ /**
65
+ * Whether to clear the local session before redirecting upstream.
66
+ * Defaults to true.
67
+ */
68
+ localSignOut?: boolean;
40
69
  }
41
70
  export interface ResolveSafeRedirectUriOptions {
42
71
  /** Origin for resolving relative paths. */
@@ -50,6 +79,7 @@ export declare function setTestingToken(token: string): void;
50
79
  export declare function getTestingToken(): string | null;
51
80
  export declare function clearTestingToken(): void;
52
81
  export declare function buildAuthUrl(authUrl: string, options?: BuildAuthUrlOptions): string;
82
+ export declare function buildFederatedLogoutUrl(authUrl: string, options?: BuildFederatedLogoutUrlOptions): string;
53
83
  export declare function resolveSafeRedirectUri(rawValue: string | null | undefined, options: ResolveSafeRedirectUriOptions): string;
54
84
  export declare class AuthManager {
55
85
  private readonly apiUrl;
@@ -112,6 +142,10 @@ export declare class AuthManager {
112
142
  * Build auth URL for login/signup/custom auth sub-path.
113
143
  */
114
144
  getAuthUrl(options?: BuildAuthUrlOptions): string;
145
+ /**
146
+ * Build upstream/federated logout URL.
147
+ */
148
+ getFederatedLogoutUrl(options?: BuildFederatedLogoutUrlOptions): string;
115
149
  /**
116
150
  * Redirect to the auth service, passing the current URL as redirect_uri.
117
151
  * After the user authenticates, the auth service should redirect back to
@@ -120,4 +154,11 @@ export declare class AuthManager {
120
154
  redirectToAuth(options?: Omit<BuildAuthUrlOptions, "redirectUri"> & {
121
155
  redirectUri?: string;
122
156
  }): void;
157
+ /**
158
+ * Optional full logout flow:
159
+ * 1. clear local SDK/session cookies
160
+ * 2. redirect to auth service logout endpoint to terminate upstream SSO
161
+ */
162
+ redirectToFederatedLogout(options?: RedirectToFederatedLogoutOptions): Promise<void>;
123
163
  }
164
+ export {};
package/dist/auth.js CHANGED
@@ -121,6 +121,26 @@ export function buildAuthUrl(authUrl, options = {}) {
121
121
  }
122
122
  return url.toString();
123
123
  }
124
+ export function buildFederatedLogoutUrl(authUrl, options = {}) {
125
+ const url = new URL(authUrl);
126
+ url.pathname = resolveAuthPath(url.pathname, options.path ?? "logout");
127
+ for (const [key, value] of Object.entries(options.params ?? {})) {
128
+ if (value === null || value === undefined)
129
+ continue;
130
+ if (Array.isArray(value)) {
131
+ url.searchParams.delete(key);
132
+ for (const item of value) {
133
+ url.searchParams.append(key, String(item));
134
+ }
135
+ continue;
136
+ }
137
+ url.searchParams.set(key, String(value));
138
+ }
139
+ if (options.redirectUri && options.redirectUri.trim()) {
140
+ url.searchParams.set(options.redirectParam ?? "redirect_uri", options.redirectUri);
141
+ }
142
+ return url.toString();
143
+ }
124
144
  export function resolveSafeRedirectUri(rawValue, options) {
125
145
  const siteOrigin = normalizeOrigin(options.siteOrigin);
126
146
  const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
@@ -368,6 +388,12 @@ export class AuthManager {
368
388
  getAuthUrl(options = {}) {
369
389
  return buildAuthUrl(this.authUrl, options);
370
390
  }
391
+ /**
392
+ * Build upstream/federated logout URL.
393
+ */
394
+ getFederatedLogoutUrl(options = {}) {
395
+ return buildFederatedLogoutUrl(this.authUrl, options);
396
+ }
371
397
  /**
372
398
  * Redirect to the auth service, passing the current URL as redirect_uri.
373
399
  * After the user authenticates, the auth service should redirect back to
@@ -380,4 +406,21 @@ export class AuthManager {
380
406
  const redirectUri = options.redirectUri ?? window.location.href;
381
407
  window.location.href = this.getAuthUrl({ ...options, redirectUri });
382
408
  }
409
+ /**
410
+ * Optional full logout flow:
411
+ * 1. clear local SDK/session cookies
412
+ * 2. redirect to auth service logout endpoint to terminate upstream SSO
413
+ */
414
+ async redirectToFederatedLogout(options = {}) {
415
+ this.assertBrowserContext();
416
+ const redirectUri = options.redirectUri ?? window.location.href;
417
+ const localSignOut = options.localSignOut ?? true;
418
+ if (localSignOut) {
419
+ await this.signOut();
420
+ }
421
+ window.location.href = this.getFederatedLogoutUrl({
422
+ ...options,
423
+ redirectUri,
424
+ });
425
+ }
383
426
  }
@@ -3,7 +3,7 @@
3
3
  "./browser.js": function (module, exports, require) {
4
4
  "use strict";
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ApiError = exports.setTestingToken = exports.resolveSafeRedirectUri = exports.getTestingToken = exports.clearTestingToken = exports.buildAuthUrl = exports.AuthManager = exports.LemmaClient = void 0;
6
+ exports.ApiError = exports.setTestingToken = exports.resolveSafeRedirectUri = exports.getTestingToken = exports.clearTestingToken = exports.buildFederatedLogoutUrl = exports.buildAuthUrl = exports.AuthManager = exports.LemmaClient = void 0;
7
7
  /**
8
8
  * Browser bundle entry point.
9
9
  * Exposes LemmaClient as globalThis.LemmaClient.LemmaClient
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "LemmaClient", { enumerable: true, get: function
19
19
  var auth_js_1 = require("./auth.js");
20
20
  Object.defineProperty(exports, "AuthManager", { enumerable: true, get: function () { return auth_js_1.AuthManager; } });
21
21
  Object.defineProperty(exports, "buildAuthUrl", { enumerable: true, get: function () { return auth_js_1.buildAuthUrl; } });
22
+ Object.defineProperty(exports, "buildFederatedLogoutUrl", { enumerable: true, get: function () { return auth_js_1.buildFederatedLogoutUrl; } });
22
23
  Object.defineProperty(exports, "clearTestingToken", { enumerable: true, get: function () { return auth_js_1.clearTestingToken; } });
23
24
  Object.defineProperty(exports, "getTestingToken", { enumerable: true, get: function () { return auth_js_1.getTestingToken; } });
24
25
  Object.defineProperty(exports, "resolveSafeRedirectUri", { enumerable: true, get: function () { return auth_js_1.resolveSafeRedirectUri; } });
@@ -198,6 +199,7 @@ exports.setTestingToken = setTestingToken;
198
199
  exports.getTestingToken = getTestingToken;
199
200
  exports.clearTestingToken = clearTestingToken;
200
201
  exports.buildAuthUrl = buildAuthUrl;
202
+ exports.buildFederatedLogoutUrl = buildFederatedLogoutUrl;
201
203
  exports.resolveSafeRedirectUri = resolveSafeRedirectUri;
202
204
  const session_1 = require("supertokens-web-js/recipe/session");
203
205
  const supertokens_js_1 = require("./supertokens.js");
@@ -305,6 +307,26 @@ function buildAuthUrl(authUrl, options = {}) {
305
307
  }
306
308
  return url.toString();
307
309
  }
310
+ function buildFederatedLogoutUrl(authUrl, options = {}) {
311
+ const url = new URL(authUrl);
312
+ url.pathname = resolveAuthPath(url.pathname, options.path ?? "logout");
313
+ for (const [key, value] of Object.entries(options.params ?? {})) {
314
+ if (value === null || value === undefined)
315
+ continue;
316
+ if (Array.isArray(value)) {
317
+ url.searchParams.delete(key);
318
+ for (const item of value) {
319
+ url.searchParams.append(key, String(item));
320
+ }
321
+ continue;
322
+ }
323
+ url.searchParams.set(key, String(value));
324
+ }
325
+ if (options.redirectUri && options.redirectUri.trim()) {
326
+ url.searchParams.set(options.redirectParam ?? "redirect_uri", options.redirectUri);
327
+ }
328
+ return url.toString();
329
+ }
308
330
  function resolveSafeRedirectUri(rawValue, options) {
309
331
  const siteOrigin = normalizeOrigin(options.siteOrigin);
310
332
  const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
@@ -549,6 +571,12 @@ class AuthManager {
549
571
  getAuthUrl(options = {}) {
550
572
  return buildAuthUrl(this.authUrl, options);
551
573
  }
574
+ /**
575
+ * Build upstream/federated logout URL.
576
+ */
577
+ getFederatedLogoutUrl(options = {}) {
578
+ return buildFederatedLogoutUrl(this.authUrl, options);
579
+ }
552
580
  /**
553
581
  * Redirect to the auth service, passing the current URL as redirect_uri.
554
582
  * After the user authenticates, the auth service should redirect back to
@@ -561,6 +589,23 @@ class AuthManager {
561
589
  const redirectUri = options.redirectUri ?? window.location.href;
562
590
  window.location.href = this.getAuthUrl({ ...options, redirectUri });
563
591
  }
592
+ /**
593
+ * Optional full logout flow:
594
+ * 1. clear local SDK/session cookies
595
+ * 2. redirect to auth service logout endpoint to terminate upstream SSO
596
+ */
597
+ async redirectToFederatedLogout(options = {}) {
598
+ this.assertBrowserContext();
599
+ const redirectUri = options.redirectUri ?? window.location.href;
600
+ const localSignOut = options.localSignOut ?? true;
601
+ if (localSignOut) {
602
+ await this.signOut();
603
+ }
604
+ window.location.href = this.getFederatedLogoutUrl({
605
+ ...options,
606
+ redirectUri,
607
+ });
608
+ }
564
609
  }
565
610
  exports.AuthManager = AuthManager;
566
611
 
@@ -1538,6 +1583,9 @@ class ConversationsNamespace {
1538
1583
  listByAssistant(assistantId, options = {}) {
1539
1584
  return this.list({ ...options, assistant_id: assistantId });
1540
1585
  }
1586
+ listModels() {
1587
+ return this.http.request("GET", "/models");
1588
+ }
1541
1589
  create(payload) {
1542
1590
  return this.http.request("POST", "/conversations", {
1543
1591
  body: {
package/dist/browser.d.ts CHANGED
@@ -9,5 +9,5 @@
9
9
  * </script>
10
10
  */
11
11
  export { LemmaClient } from "./client.js";
12
- export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
12
+ export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
13
13
  export { ApiError } from "./http.js";
package/dist/browser.js CHANGED
@@ -9,5 +9,5 @@
9
9
  * </script>
10
10
  */
11
11
  export { LemmaClient } from "./client.js";
12
- export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
12
+ export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
13
13
  export { ApiError } from "./http.js";
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { LemmaClient } from "./client.js";
2
2
  export type { LemmaConfig } from "./client.js";
3
- export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
4
- export type { AuthState, AuthListener, AuthStatus, UserInfo, AuthRedirectMode, BuildAuthUrlOptions, ResolveSafeRedirectUriOptions, } from "./auth.js";
3
+ export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
4
+ export type { AuthState, AuthListener, AuthStatus, UserInfo, AuthRedirectMode, BuildAuthUrlOptions, BuildFederatedLogoutUrlOptions, RedirectToFederatedLogoutOptions, ResolveSafeRedirectUriOptions, } from "./auth.js";
5
5
  export { ApiError } from "./http.js";
6
6
  export * from "./types.js";
7
7
  export { readSSE, parseSSEJson } from "./streams.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { LemmaClient } from "./client.js";
2
- export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
2
+ export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
3
3
  export { ApiError } from "./http.js";
4
4
  export * from "./types.js";
5
5
  export { readSSE, parseSSEJson } from "./streams.js";
@@ -1,4 +1,5 @@
1
1
  import type { HttpClient } from "../http.js";
2
+ import type { AvailableModelsListResponse } from "../openapi_client/models/AvailableModelsListResponse.js";
2
3
  import type { AssistantListResponse } from "../openapi_client/models/AssistantListResponse.js";
3
4
  import type { AssistantResponse } from "../openapi_client/models/AssistantResponse.js";
4
5
  import type { ConversationListResponse } from "../openapi_client/models/ConversationListResponse.js";
@@ -41,6 +42,7 @@ export declare class ConversationsNamespace {
41
42
  limit?: number;
42
43
  page_token?: string;
43
44
  }): Promise<ConversationListResponse>;
45
+ listModels(): Promise<AvailableModelsListResponse>;
44
46
  create(payload: CreateConversationRequest): Promise<ConversationResponse>;
45
47
  createForAssistant(assistantId: string, payload?: Omit<CreateConversationRequest, "assistant_id">): Promise<ConversationResponse>;
46
48
  get(conversationId: string, options?: {
@@ -67,6 +67,9 @@ export class ConversationsNamespace {
67
67
  listByAssistant(assistantId, options = {}) {
68
68
  return this.list({ ...options, assistant_id: assistantId });
69
69
  }
70
+ listModels() {
71
+ return this.http.request("GET", "/models");
72
+ }
70
73
  create(payload) {
71
74
  return this.http.request("POST", "/conversations", {
72
75
  body: {
@@ -26,7 +26,9 @@ export type { AssistantListResponse } from './models/AssistantListResponse.js';
26
26
  export type { AssistantResponse } from './models/AssistantResponse.js';
27
27
  export type { AssistantSurfaceListResponse } from './models/AssistantSurfaceListResponse.js';
28
28
  export type { AssistantSurfaceResponse } from './models/AssistantSurfaceResponse.js';
29
+ export type { AvailableModelInfo } from './models/AvailableModelInfo.js';
29
30
  export { AvailableModels } from './models/AvailableModels.js';
31
+ export type { AvailableModelsListResponse } from './models/AvailableModelsListResponse.js';
30
32
  export { BillingInterval } from './models/BillingInterval.js';
31
33
  export type { Body_upload_file_files__resource_type___resource_id__upload_post } from './models/Body_upload_file_files__resource_type___resource_id__upload_post.js';
32
34
  export type { BulkCreateRecordsRequest } from './models/BulkCreateRecordsRequest.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Information about an available model.
3
+ */
4
+ export type AvailableModelInfo = {
5
+ id: string;
6
+ name: string;
7
+ provider_model_name: string;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -4,8 +4,7 @@ export declare enum AvailableModels {
4
4
  GEMINI_FLASH_LITE = "GEMINI_FLASH_LITE",
5
5
  KIMI_K2 = "KIMI_K2",
6
6
  GPT_OSS = "GPT_OSS",
7
- DEEPSEEK_V31 = "DEEPSEEK_V31",
8
- CLAUDE_SONNET_4 = "CLAUDE_SONNET_4",
9
- QWEN3_235B = "QWEN3_235B",
10
- GLM_5 = "GLM_5"
7
+ DEEPSEEK_V32 = "DEEPSEEK_V32",
8
+ GLM_5 = "GLM_5",
9
+ QWEN3_6 = "QWEN3_6"
11
10
  }
@@ -9,8 +9,7 @@ export var AvailableModels;
9
9
  AvailableModels["GEMINI_FLASH_LITE"] = "GEMINI_FLASH_LITE";
10
10
  AvailableModels["KIMI_K2"] = "KIMI_K2";
11
11
  AvailableModels["GPT_OSS"] = "GPT_OSS";
12
- AvailableModels["DEEPSEEK_V31"] = "DEEPSEEK_V31";
13
- AvailableModels["CLAUDE_SONNET_4"] = "CLAUDE_SONNET_4";
14
- AvailableModels["QWEN3_235B"] = "QWEN3_235B";
12
+ AvailableModels["DEEPSEEK_V32"] = "DEEPSEEK_V32";
15
13
  AvailableModels["GLM_5"] = "GLM_5";
14
+ AvailableModels["QWEN3_6"] = "QWEN3_6";
16
15
  })(AvailableModels || (AvailableModels = {}));
@@ -0,0 +1,7 @@
1
+ import type { AvailableModelInfo } from './AvailableModelInfo.js';
2
+ /**
3
+ * Response containing list of available models.
4
+ */
5
+ export type AvailableModelsListResponse = {
6
+ items: Array<AvailableModelInfo>;
7
+ };
@@ -16,4 +16,6 @@ export type FunctionRunResponse = {
16
16
  status: FunctionRunStatus;
17
17
  user_email?: (string | null);
18
18
  user_id: string;
19
+ workspace_process_id?: (string | null);
20
+ workspace_session_id?: (string | null);
19
21
  };
@@ -1,3 +1,4 @@
1
+ import type { AvailableModelsListResponse } from '../models/AvailableModelsListResponse.js';
1
2
  import type { ConversationListResponse } from '../models/ConversationListResponse.js';
2
3
  import type { ConversationMessageListResponse } from '../models/ConversationMessageListResponse.js';
3
4
  import type { ConversationResponse } from '../models/ConversationResponse.js';
@@ -76,4 +77,11 @@ export declare class ConversationsService {
76
77
  * @throws ApiError
77
78
  */
78
79
  static conversationStreamResume(conversationId: string, podId?: (string | null)): CancelablePromise<any>;
80
+ /**
81
+ * List Available Models
82
+ * Get list of all available models in the system.
83
+ * @returns AvailableModelsListResponse Successful Response
84
+ * @throws ApiError
85
+ */
86
+ static conversationModelsList(): CancelablePromise<AvailableModelsListResponse>;
79
87
  }
@@ -185,4 +185,16 @@ export class ConversationsService {
185
185
  },
186
186
  });
187
187
  }
188
+ /**
189
+ * List Available Models
190
+ * Get list of all available models in the system.
191
+ * @returns AvailableModelsListResponse Successful Response
192
+ * @throws ApiError
193
+ */
194
+ static conversationModelsList() {
195
+ return __request(OpenAPI, {
196
+ method: 'GET',
197
+ url: '/models',
198
+ });
199
+ }
188
200
  }
@@ -773,7 +773,15 @@ export function AssistantExperienceView({ controller, title = "Lemma Assistant",
773
773
  const isPinnedToBottomRef = useRef(true);
774
774
  const loadingOlderFromScrollRef = useRef(false);
775
775
  const isConversationBusy = controller.isLoading || controller.isActiveConversationRunning;
776
- const availableModels = useMemo(() => Object.values(AvailableModels), []);
776
+ const availableModels = useMemo(() => {
777
+ const dynamicModels = controller.availableModels
778
+ .map((model) => model.id)
779
+ .filter((model) => model.trim().length > 0);
780
+ return dynamicModels.length > 0
781
+ ? dynamicModels
782
+ : Object.values(AvailableModels);
783
+ }, [controller.availableModels]);
784
+ const availableModelLabels = useMemo(() => new Map(controller.availableModels.map((model) => [model.id, model.name])), [controller.availableModels]);
777
785
  const resizeComposer = useCallback(() => {
778
786
  const textarea = inputRef.current;
779
787
  if (!textarea)
@@ -1027,7 +1035,7 @@ export function AssistantExperienceView({ controller, title = "Lemma Assistant",
1027
1035
  return (_jsxs("div", { className: "lemma-assistant-experience", "data-chrome-style": chromeStyle, "data-status-placement": statusPlacement, "data-radius": radius, "data-show-model-picker": showModelPicker ? "true" : "false", "data-busy": isConversationBusy ? "true" : "false", "data-has-plan": planSummary ? "true" : "false", "data-has-pending-files": controller.pendingFiles.length > 0 ? "true" : "false", "data-show-conversation-list": showConversationList ? "true" : "false", children: [showConversationList ? (_jsxs("aside", { className: "lemma-assistant-experience-sidebar", children: [_jsx("div", { className: "lemma-assistant-experience-sidebar-header", children: _jsxs("div", { className: "lemma-assistant-experience-sidebar-header-row", children: [_jsxs("div", { className: "lemma-assistant-experience-sidebar-copy", children: [_jsx("div", { className: "lemma-assistant-experience-sidebar-title", children: "Conversations" }), _jsxs("div", { className: "lemma-assistant-experience-sidebar-meta", children: [controller.conversations.length, " total"] })] }), showNewConversationButton ? (_jsx("button", { type: "button", onClick: controller.clearMessages, className: "lemma-assistant-experience-sidebar-new", children: "New" })) : null] }) }), _jsx("div", { className: "lemma-assistant-experience-sidebar-items", children: controller.conversations.map((conversation) => {
1028
1036
  const isActive = conversation.id === controller.activeConversationId;
1029
1037
  return (_jsxs("button", { type: "button", onClick: () => controller.selectConversation(conversation.id), className: cx("lemma-assistant-experience-sidebar-item", isActive && "lemma-assistant-experience-sidebar-item-active"), children: [_jsx("div", { className: "lemma-assistant-experience-sidebar-item-title", children: renderConversationLabel({ conversation, isActive }) }), _jsx("div", { className: "lemma-assistant-experience-sidebar-item-status", children: (conversation.status || "waiting").toLowerCase() })] }, conversation.id));
1030
- }) })] })) : null, _jsxs("div", { className: "lemma-assistant-experience-main", children: [_jsxs("div", { className: "lemma-assistant-experience-card", children: [_jsx(AssistantHeader, { className: "lemma-assistant-experience-header", tone: headerTone, title: title, subtitle: subtitle, badge: _jsx("span", { className: "lemma-assistant-experience-header-badge-icon", children: "\u2728" }), controls: showModelPicker || showNewConversationButton ? (_jsxs(_Fragment, { children: [showModelPicker ? (_jsx(AssistantModelPicker, { value: controller.conversationModel, options: availableModels, onChange: (nextModel) => { void handleModelChange(nextModel); }, disabled: isConversationBusy || isUpdatingModel, autoLabel: "Auto", className: "lemma-assistant-experience-model-picker" })) : null, showNewConversationButton ? (_jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "lemma-assistant-experience-new", children: "\u21BA" })) : null] })) : undefined }), _jsxs(AssistantMessageViewport, { className: "lemma-assistant-experience-viewport", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || (_jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); }, suggestions: emptyStateSuggestions }))) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading", children: _jsx("span", { className: "lemma-assistant-experience-loading-text", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading-older", children: _jsx("span", { className: "lemma-assistant-experience-loading-older-text", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
1038
+ }) })] })) : null, _jsxs("div", { className: "lemma-assistant-experience-main", children: [_jsxs("div", { className: "lemma-assistant-experience-card", children: [_jsx(AssistantHeader, { className: "lemma-assistant-experience-header", tone: headerTone, title: title, subtitle: subtitle, badge: _jsx("span", { className: "lemma-assistant-experience-header-badge-icon", children: "\u2728" }), controls: showModelPicker || showNewConversationButton ? (_jsxs(_Fragment, { children: [showModelPicker ? (_jsx(AssistantModelPicker, { value: controller.conversationModel, options: availableModels, getOptionLabel: (model) => availableModelLabels.get(model) ?? model, onChange: (nextModel) => { void handleModelChange(nextModel); }, disabled: isConversationBusy || isUpdatingModel, autoLabel: "Auto", className: "lemma-assistant-experience-model-picker" })) : null, showNewConversationButton ? (_jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "lemma-assistant-experience-new", children: "\u21BA" })) : null] })) : undefined }), _jsxs(AssistantMessageViewport, { className: "lemma-assistant-experience-viewport", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || (_jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); }, suggestions: emptyStateSuggestions }))) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading", children: _jsx("span", { className: "lemma-assistant-experience-loading-text", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading-older", children: _jsx("span", { className: "lemma-assistant-experience-loading-older-text", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
1031
1039
  const previousRow = index > 0 ? displayMessageRows[index - 1] : null;
1032
1040
  const showAssistantHeader = row.message.role !== "assistant"
1033
1041
  ? false
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { AvailableModelInfo } from "../../types.js";
2
3
  import type { AssistantRenderableMessage, AssistantToolInvocation } from "../useAssistantController.js";
3
4
  export interface AssistantConversationListItem {
4
5
  id: string;
@@ -11,6 +12,7 @@ export interface AssistantControllerView {
11
12
  messages: AssistantRenderableMessage[];
12
13
  conversations: AssistantConversationListItem[];
13
14
  activeConversationId: string | null;
15
+ availableModels: AvailableModelInfo[];
14
16
  conversationModel: string | null;
15
17
  setConversationModel(model: string | null): Promise<void>;
16
18
  isActiveConversationRunning: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { LemmaClient } from "../client.js";
2
- import type { Conversation, ConversationModel } from "../types.js";
2
+ import type { AvailableModelInfo, Conversation, ConversationModel } from "../types.js";
3
3
  export interface AssistantConversationScope {
4
4
  podId?: string | null;
5
5
  assistantId?: string | null;
@@ -54,6 +54,7 @@ export interface UseAssistantControllerResult {
54
54
  messages: AssistantRenderableMessage[];
55
55
  conversations: Conversation[];
56
56
  activeConversationId: string | null;
57
+ availableModels: AvailableModelInfo[];
57
58
  conversationModel: ConversationModel | null;
58
59
  isActiveConversationRunning: boolean;
59
60
  isLoading: boolean;
@@ -543,6 +543,7 @@ export function useAssistantController({ client, podId, assistantId, organizatio
543
543
  const [messages, setMessages] = useState([]);
544
544
  const [conversations, setConversations] = useState([]);
545
545
  const [activeConversationId, setActiveConversationId] = useState(null);
546
+ const [availableModels, setAvailableModels] = useState([]);
546
547
  const [conversationModel, setConversationModelState] = useState(null);
547
548
  const [isStreaming, setIsStreaming] = useState(false);
548
549
  const [isLoadingConversations, setIsLoadingConversations] = useState(false);
@@ -649,6 +650,15 @@ export function useAssistantController({ client, podId, assistantId, organizatio
649
650
  setIsLoadingConversations(false);
650
651
  }
651
652
  }, [scope, sessionListConversations]);
653
+ const loadAvailableModels = useCallback(async () => {
654
+ try {
655
+ const response = await client.conversations.listModels();
656
+ return response.items ?? [];
657
+ }
658
+ catch {
659
+ return [];
660
+ }
661
+ }, [client]);
652
662
  const loadConversationMessages = useCallback(async (conversationId) => {
653
663
  setIsLoadingMessages(true);
654
664
  try {
@@ -706,6 +716,23 @@ export function useAssistantController({ client, podId, assistantId, organizatio
706
716
  useEffect(() => {
707
717
  conversationsRef.current = conversations;
708
718
  }, [conversations]);
719
+ useEffect(() => {
720
+ if (!enabled) {
721
+ setAvailableModels([]);
722
+ return;
723
+ }
724
+ let cancelled = false;
725
+ void loadAvailableModels()
726
+ .then((models) => {
727
+ if (cancelled)
728
+ return;
729
+ setAvailableModels(models);
730
+ })
731
+ .catch(() => undefined);
732
+ return () => {
733
+ cancelled = true;
734
+ };
735
+ }, [enabled, loadAvailableModels]);
709
736
  useEffect(() => {
710
737
  const conversationId = activeConversationIdRef.current;
711
738
  if (!conversationId) {
@@ -759,6 +786,7 @@ export function useAssistantController({ client, podId, assistantId, organizatio
759
786
  loadingConversationIdRef.current = null;
760
787
  skipInitialLoadConversationIdsRef.current.clear();
761
788
  setActiveConversationId(null);
789
+ setAvailableModels([]);
762
790
  setConversationModelState(null);
763
791
  setConversations([]);
764
792
  setMessages([]);
@@ -842,8 +870,12 @@ export function useAssistantController({ client, podId, assistantId, organizatio
842
870
  const conversationIsRunning = isConversationRunning(activeConversation?.status);
843
871
  if (!hadActiveStream && !conversationIsRunning)
844
872
  return;
873
+ const previousStatus = activeConversation?.status;
845
874
  touchConversation(conversationId, { status: "waiting" });
846
- void sessionStop(conversationId).catch(() => undefined);
875
+ void sessionStop(conversationId).catch((error) => {
876
+ touchConversation(conversationId, { status: previousStatus });
877
+ setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to stop conversation"));
878
+ });
847
879
  }, [isStreaming, sessionCancel, sessionIsStreaming, sessionStop, touchConversation]);
848
880
  const selectConversation = useCallback((conversationId) => {
849
881
  if (sessionIsStreaming || isStreaming) {
@@ -1066,6 +1098,7 @@ export function useAssistantController({ client, podId, assistantId, organizatio
1066
1098
  messages,
1067
1099
  conversations,
1068
1100
  activeConversationId,
1101
+ availableModels,
1069
1102
  conversationModel,
1070
1103
  isActiveConversationRunning,
1071
1104
  isLoading,
@@ -21,6 +21,7 @@ function messageTime(message) {
21
21
  function isOptimisticId(messageId) {
22
22
  return messageId.startsWith("optimistic-user-");
23
23
  }
24
+ const OPTIMISTIC_MATCH_WINDOW_MS = 2 * 60 * 1000;
24
25
  function upsertRuntimeMessage(previous, incoming) {
25
26
  const next = [...previous];
26
27
  const directIndex = next.findIndex((message) => message.id === incoming.id);
@@ -31,9 +32,22 @@ function upsertRuntimeMessage(previous, incoming) {
31
32
  if (incoming.role === "user") {
32
33
  const incomingText = messageText(incoming.content);
33
34
  if (incomingText) {
34
- const optimisticIndex = next.findIndex((message) => (message.role === "user"
35
- && isOptimisticId(message.id)
36
- && messageText(message.content) === incomingText));
35
+ const incomingTimestamp = messageTime(incoming);
36
+ let optimisticIndex = -1;
37
+ let bestDistance = Number.POSITIVE_INFINITY;
38
+ next.forEach((message, index) => {
39
+ if (message.role !== "user"
40
+ || !isOptimisticId(message.id)
41
+ || messageText(message.content) !== incomingText) {
42
+ return;
43
+ }
44
+ const distance = Math.abs(messageTime(message) - incomingTimestamp);
45
+ if (distance > OPTIMISTIC_MATCH_WINDOW_MS || distance >= bestDistance) {
46
+ return;
47
+ }
48
+ optimisticIndex = index;
49
+ bestDistance = distance;
50
+ });
37
51
  if (optimisticIndex >= 0) {
38
52
  next[optimisticIndex] = incoming;
39
53
  return next;
@@ -71,7 +85,14 @@ export function useAssistantRuntime({ conversationId = null, sessionMessages = [
71
85
  const normalized = messages
72
86
  .map((message) => toRuntimeMessage(message, conversationId))
73
87
  .filter((message) => !conversationId || message.conversation_id === conversationId);
74
- setRuntimeMessages([...normalized].sort((a, b) => messageTime(a) - messageTime(b)));
88
+ setRuntimeMessages((previous) => {
89
+ const scopedPrevious = previous.filter((message) => !conversationId || message.conversation_id === conversationId);
90
+ // Loads can complete after optimistic appends or stream events. Merge the
91
+ // loaded snapshot into the current runtime state so newer local messages
92
+ // are not temporarily dropped while the server catches up.
93
+ const merged = normalized.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, message), scopedPrevious);
94
+ return [...merged].sort((a, b) => messageTime(a) - messageTime(b));
95
+ });
75
96
  }, [conversationId]);
76
97
  const appendOptimisticUserMessage = useCallback((content, options) => {
77
98
  const trimmed = content.trim();
@@ -393,12 +393,21 @@ export function useAssistantSession(options) {
393
393
  return false;
394
394
  }
395
395
  }
396
+ const previousResumeKey = autoResumedKeyRef.current;
396
397
  autoResumedKeyRef.current = resumeKey;
397
- await resume({
398
- conversationId: id,
399
- onlyIfRunning: true,
400
- });
401
- return true;
398
+ try {
399
+ await resume({
400
+ conversationId: id,
401
+ onlyIfRunning: true,
402
+ });
403
+ return true;
404
+ }
405
+ catch (error) {
406
+ if (autoResumedKeyRef.current === resumeKey) {
407
+ autoResumedKeyRef.current = previousResumeKey;
408
+ }
409
+ throw error;
410
+ }
402
411
  }, [conversationId, isStreaming, refreshConversation, resume]);
403
412
  const stop = useCallback(async (explicitConversationId) => {
404
413
  const id = requireConversationId(explicitConversationId ?? conversationId);
package/dist/types.d.ts CHANGED
@@ -40,7 +40,7 @@ export type CreateAssistantInput = CreateAssistantRequest;
40
40
  export type UpdateAssistantInput = UpdateAssistantRequest;
41
41
  export type Conversation = ConversationResponse;
42
42
  export type ConversationMessage = ConversationMessageResponse;
43
- export type ConversationModel = `${AvailableModels}`;
43
+ export type ConversationModel = `${AvailableModels}` | (string & {});
44
44
  export type Task = TaskResponse;
45
45
  export type TaskMessage = TaskMessageResponse;
46
46
  export type FunctionRun = FunctionRunResponse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemma-sdk",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Official TypeScript SDK for Lemma pod-scoped APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",