@nextclaw/ui 0.9.2 → 0.9.4

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 (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/ChannelsList-DDfZIiJa.js +1 -0
  3. package/dist/assets/ChatPage-FpRraTxm.js +38 -0
  4. package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-Kndx8OJj.js} +1 -1
  5. package/dist/assets/LogoBadge-hKHoLH9n.js +1 -0
  6. package/dist/assets/MarketplacePage-CZIJyfjK.js +49 -0
  7. package/dist/assets/McpMarketplacePage-BGrAMA37.js +40 -0
  8. package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BpKQeGfb.js} +1 -1
  9. package/dist/assets/ProvidersList-qfUL6mrW.js +1 -0
  10. package/dist/assets/RemoteAccessPage-BQuMsngI.js +1 -0
  11. package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-CVlqNWKO.js} +1 -1
  12. package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-DXFV6Mvx.js} +1 -1
  13. package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-BGW9aUqv.js} +2 -2
  14. package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-BByfa1ke.js} +2 -2
  15. package/dist/assets/{chat-message-AWIcksDK.js → chat-message-ZwnDwDuQ.js} +1 -1
  16. package/dist/assets/index-BWvap_iq.js +8 -0
  17. package/dist/assets/index-COrhpAdh.css +1 -0
  18. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  19. package/dist/assets/{label-DD61y-4v.js → label-Bklr3fXc.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-CfnoVycc.js → page-layout-sNhcbwtm.js} +1 -1
  22. package/dist/assets/{popover-DsugZ6rp.js → popover-C3rJrJJG.js} +1 -1
  23. package/dist/assets/{security-config-DIrf2Z0O.js → security-config-BueosYw1.js} +1 -1
  24. package/dist/assets/skeleton-CiG6msbm.js +1 -0
  25. package/dist/assets/status-dot-CsIV5YrS.js +1 -0
  26. package/dist/assets/{switch-NX5OmUXQ.js → switch-DSdHSIsC.js} +1 -1
  27. package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-BB-VjdL2.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BL5s8KDC.js} +2 -2
  29. package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
  30. package/dist/index.html +3 -3
  31. package/package.json +3 -3
  32. package/src/App.tsx +4 -0
  33. package/src/api/auth.types.ts +24 -0
  34. package/src/api/chat-session-type.types.ts +21 -0
  35. package/src/api/marketplace.ts +8 -2
  36. package/src/api/mcp-marketplace.ts +138 -0
  37. package/src/api/remote.ts +77 -0
  38. package/src/api/remote.types.ts +104 -0
  39. package/src/api/types.ts +28 -34
  40. package/src/components/chat/ChatSidebar.test.tsx +31 -2
  41. package/src/components/chat/ChatSidebar.tsx +26 -2
  42. package/src/components/chat/chat-page-data.ts +36 -38
  43. package/src/components/chat/chat-page-runtime.test.ts +96 -2
  44. package/src/components/chat/chat-page-runtime.ts +1 -135
  45. package/src/components/chat/chat-session-preference-governance.ts +303 -0
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
  47. package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
  48. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
  49. package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
  50. package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
  51. package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
  52. package/src/components/chat/stores/chat-input.store.ts +14 -1
  53. package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
  54. package/src/components/chat/useChatSessionTypeState.ts +55 -12
  55. package/src/components/layout/Sidebar.tsx +11 -1
  56. package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
  57. package/src/components/marketplace/MarketplacePage.tsx +52 -199
  58. package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
  59. package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
  60. package/src/components/marketplace/marketplace-localization.ts +77 -0
  61. package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
  62. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
  63. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
  64. package/src/components/remote/RemoteAccessPage.tsx +396 -0
  65. package/src/components/ui/input.tsx +1 -1
  66. package/src/components/ui/label.tsx +1 -1
  67. package/src/hooks/useMarketplace.ts +36 -7
  68. package/src/hooks/useMcpMarketplace.ts +99 -0
  69. package/src/hooks/useRemoteAccess.ts +120 -0
  70. package/src/hooks/useWebSocket.ts +25 -16
  71. package/src/lib/i18n.marketplace.ts +91 -0
  72. package/src/lib/i18n.remote.ts +142 -0
  73. package/src/lib/i18n.ts +10 -68
  74. package/dist/assets/ChannelsList-DKD6Llid.js +0 -1
  75. package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
  76. package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
  77. package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
  78. package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
  79. package/dist/assets/index-BEgClaDH.js +0 -8
  80. package/dist/assets/index-C8GsgIUn.css +0 -1
  81. package/dist/assets/skeleton-DJ-Wen2o.js +0 -1
@@ -4,9 +4,11 @@ import type { SessionEntryView, ThinkingLevel } from '@/api/types';
4
4
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
5
  import { useChatSessionTypeState } from '@/components/chat/useChatSessionTypeState';
6
6
  import {
7
+ resolveRecentSessionPreferredThinking,
7
8
  resolveRecentSessionPreferredModel,
8
- useSyncSelectedModel
9
- } from '@/components/chat/chat-page-runtime';
9
+ useSyncSelectedModel,
10
+ useSyncSelectedThinking
11
+ } from '@/components/chat/chat-session-preference-governance';
10
12
  import {
11
13
  useChatCapabilities,
12
14
  useChatSessionTypes,
@@ -22,24 +24,13 @@ type UseChatPageDataParams = {
22
24
  query: string;
23
25
  selectedSessionKey: string | null;
24
26
  selectedAgentId: string;
27
+ currentSelectedModel: string;
25
28
  pendingSessionType: string;
26
29
  setPendingSessionType: Dispatch<SetStateAction<string>>;
27
30
  setSelectedModel: Dispatch<SetStateAction<string>>;
31
+ setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
28
32
  };
29
33
 
30
- const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
31
-
32
- function parseThinkingLevel(value: unknown): ThinkingLevel | null {
33
- if (typeof value !== 'string') {
34
- return null;
35
- }
36
- const normalized = value.trim().toLowerCase();
37
- if (!normalized) {
38
- return null;
39
- }
40
- return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
41
- }
42
-
43
34
  export function useChatPageData(params: UseChatPageDataParams) {
44
35
  const configQuery = useConfig();
45
36
  const configMetaQuery = useConfigMeta();
@@ -110,6 +101,27 @@ export function useChatPageData(params: UseChatPageDataParams) {
110
101
  }),
111
102
  [params.selectedSessionKey, sessionTypeState.selectedSessionType, sessions]
112
103
  );
104
+ const currentModelOption = useMemo(
105
+ () => modelOptions.find((option) => option.value === params.currentSelectedModel),
106
+ [modelOptions, params.currentSelectedModel]
107
+ );
108
+ const supportedThinkingLevels = useMemo(
109
+ () => (currentModelOption?.thinkingCapability?.supported as ThinkingLevel[] | undefined) ?? [],
110
+ [currentModelOption?.thinkingCapability?.supported]
111
+ );
112
+ const defaultThinkingLevel = useMemo(
113
+ () => (currentModelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
114
+ [currentModelOption?.thinkingCapability?.default]
115
+ );
116
+ const recentSessionPreferredThinking = useMemo(
117
+ () =>
118
+ resolveRecentSessionPreferredThinking({
119
+ sessions,
120
+ selectedSessionKey: params.selectedSessionKey,
121
+ sessionType: sessionTypeState.selectedSessionType
122
+ }),
123
+ [params.selectedSessionKey, sessionTypeState.selectedSessionType, sessions]
124
+ );
113
125
 
114
126
  useSyncSelectedModel({
115
127
  modelOptions,
@@ -120,30 +132,17 @@ export function useChatPageData(params: UseChatPageDataParams) {
120
132
  defaultModel: configQuery.data?.agents.defaults.model,
121
133
  setSelectedModel: params.setSelectedModel
122
134
  });
135
+ useSyncSelectedThinking({
136
+ supportedThinkingLevels,
137
+ selectedSessionKey: params.selectedSessionKey,
138
+ selectedSessionExists: Boolean(selectedSession),
139
+ selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
140
+ fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
141
+ defaultThinkingLevel,
142
+ setSelectedThinkingLevel: params.setSelectedThinkingLevel
143
+ });
123
144
 
124
145
  const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
125
- const selectedSessionThinkingLevel = useMemo(() => {
126
- if (!params.selectedSessionKey) {
127
- return null;
128
- }
129
- const metadata = historyQuery.data?.metadata;
130
- if (!metadata || typeof metadata !== 'object') {
131
- return null;
132
- }
133
- const candidates = [
134
- metadata.preferred_thinking,
135
- metadata.thinking,
136
- metadata.thinking_level,
137
- metadata.thinkingLevel
138
- ];
139
- for (const value of candidates) {
140
- const level = parseThinkingLevel(value);
141
- if (level) {
142
- return level;
143
- }
144
- }
145
- return null;
146
- }, [historyQuery.data?.metadata, params.selectedSessionKey]);
147
146
 
148
147
  return {
149
148
  configQuery,
@@ -159,7 +158,6 @@ export function useChatPageData(params: UseChatPageDataParams) {
159
158
  skillRecords,
160
159
  selectedSession,
161
160
  historyMessages,
162
- selectedSessionThinkingLevel,
163
161
  ...sessionTypeState
164
162
  };
165
163
  }
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type { SessionEntryView } from '@/api/types';
3
- import { resolveRecentSessionPreferredModel, resolveSelectedModelValue } from '@/components/chat/chat-page-runtime';
2
+ import type { SessionEntryView, ThinkingLevel } from '@/api/types';
3
+ import {
4
+ resolveRecentSessionPreferredModel,
5
+ resolveRecentSessionPreferredThinking,
6
+ resolveSelectedModelValue,
7
+ resolveSelectedThinkingLevelValue
8
+ } from '@/components/chat/chat-session-preference-governance';
4
9
 
5
10
  const modelOptions = [
6
11
  {
@@ -27,11 +32,16 @@ function createSession(overrides: Partial<SessionEntryView> & Pick<SessionEntryV
27
32
  messageCount: overrides.messageCount ?? 0,
28
33
  ...(overrides.label ? { label: overrides.label } : {}),
29
34
  ...(overrides.preferredModel ? { preferredModel: overrides.preferredModel } : {}),
35
+ ...(Object.prototype.hasOwnProperty.call(overrides, 'preferredThinking')
36
+ ? { preferredThinking: overrides.preferredThinking ?? null }
37
+ : {}),
30
38
  ...(overrides.lastRole ? { lastRole: overrides.lastRole } : {}),
31
39
  ...(overrides.lastTimestamp ? { lastTimestamp: overrides.lastTimestamp } : {})
32
40
  };
33
41
  }
34
42
 
43
+ const thinkingLevels: ThinkingLevel[] = ['off', 'minimal', 'medium', 'high'];
44
+
35
45
  describe('resolveSelectedModelValue', () => {
36
46
  it('keeps the current selected model when it is still available', () => {
37
47
  expect(
@@ -205,3 +215,87 @@ describe('resolveRecentSessionPreferredModel', () => {
205
215
  ).toBe('anthropic/claude-sonnet-4');
206
216
  });
207
217
  });
218
+
219
+ describe('resolveSelectedThinkingLevelValue', () => {
220
+ it('keeps the current selected thinking when it is still valid', () => {
221
+ expect(
222
+ resolveSelectedThinkingLevelValue({
223
+ currentSelectedThinkingLevel: 'high',
224
+ supportedThinkingLevels: thinkingLevels,
225
+ selectedSessionPreferredThinking: 'medium',
226
+ fallbackPreferredThinking: 'minimal',
227
+ defaultThinkingLevel: 'off'
228
+ })
229
+ ).toBe('high');
230
+ });
231
+
232
+ it('prefers the persisted session thinking after switching sessions', () => {
233
+ expect(
234
+ resolveSelectedThinkingLevelValue({
235
+ currentSelectedThinkingLevel: 'high',
236
+ supportedThinkingLevels: thinkingLevels,
237
+ selectedSessionPreferredThinking: 'medium',
238
+ fallbackPreferredThinking: 'minimal',
239
+ defaultThinkingLevel: 'off',
240
+ preferSessionPreferredThinking: true
241
+ })
242
+ ).toBe('medium');
243
+ });
244
+
245
+ it('preserves the current valid thinking when a draft session materializes before metadata exists', () => {
246
+ expect(
247
+ resolveSelectedThinkingLevelValue({
248
+ currentSelectedThinkingLevel: 'high',
249
+ supportedThinkingLevels: thinkingLevels,
250
+ fallbackPreferredThinking: 'minimal',
251
+ defaultThinkingLevel: 'off',
252
+ preferSessionPreferredThinking: true,
253
+ preserveCurrentSelectedThinkingOnSessionChange: true
254
+ })
255
+ ).toBe('high');
256
+ });
257
+
258
+ it('falls back to the model default when no current or persisted thinking is valid', () => {
259
+ expect(
260
+ resolveSelectedThinkingLevelValue({
261
+ currentSelectedThinkingLevel: null,
262
+ supportedThinkingLevels: thinkingLevels,
263
+ fallbackPreferredThinking: null,
264
+ defaultThinkingLevel: 'medium'
265
+ })
266
+ ).toBe('medium');
267
+ });
268
+ });
269
+
270
+ describe('resolveRecentSessionPreferredThinking', () => {
271
+ it('returns the most recent preferred thinking from the same runtime', () => {
272
+ const sessions = [
273
+ createSession({
274
+ key: 'native-1',
275
+ sessionType: 'native',
276
+ preferredThinking: 'low',
277
+ updatedAt: '2026-03-18T01:00:00.000Z'
278
+ }),
279
+ createSession({
280
+ key: 'codex-1',
281
+ sessionType: 'codex',
282
+ preferredThinking: 'high',
283
+ updatedAt: '2026-03-18T03:00:00.000Z'
284
+ }),
285
+ createSession({
286
+ key: 'codex-2',
287
+ sessionType: 'codex',
288
+ preferredThinking: 'medium',
289
+ updatedAt: '2026-03-18T02:00:00.000Z'
290
+ })
291
+ ];
292
+
293
+ expect(
294
+ resolveRecentSessionPreferredThinking({
295
+ sessions,
296
+ selectedSessionKey: 'draft',
297
+ sessionType: 'codex'
298
+ })
299
+ ).toBe('high');
300
+ });
301
+ });
@@ -1,144 +1,10 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
- import type { Dispatch, SetStateAction } from 'react';
3
- import type { ChatRunView, SessionEntryView } from '@/api/types';
4
- import type { ChatModelOption } from '@/components/chat/chat-input.types';
2
+ import type { ChatRunView } from '@/api/types';
5
3
  import { useChatRuns } from '@/hooks/useConfig';
6
4
  import { buildActiveRunBySessionKey, buildSessionRunStatusByKey } from '@/lib/session-run-status';
7
5
 
8
6
  export type ChatMainPanelView = 'chat' | 'cron' | 'skills';
9
7
 
10
- function normalizeSessionType(value: string | null | undefined): string {
11
- const normalized = value?.trim().toLowerCase();
12
- return normalized || 'native';
13
- }
14
-
15
- function hasModelOption(modelOptions: ChatModelOption[], value: string | null | undefined): value is string {
16
- const normalized = value?.trim();
17
- if (!normalized) {
18
- return false;
19
- }
20
- return modelOptions.some((option) => option.value === normalized);
21
- }
22
-
23
- export function resolveSelectedModelValue(params: {
24
- currentSelectedModel?: string;
25
- modelOptions: ChatModelOption[];
26
- selectedSessionPreferredModel?: string;
27
- fallbackPreferredModel?: string;
28
- defaultModel?: string;
29
- preferSessionPreferredModel?: boolean;
30
- preserveCurrentSelectedModelOnSessionChange?: boolean;
31
- }): string {
32
- const {
33
- currentSelectedModel,
34
- modelOptions,
35
- selectedSessionPreferredModel,
36
- fallbackPreferredModel,
37
- defaultModel,
38
- preferSessionPreferredModel = false,
39
- preserveCurrentSelectedModelOnSessionChange = false
40
- } = params;
41
- if (modelOptions.length === 0) {
42
- return '';
43
- }
44
- if (
45
- hasModelOption(modelOptions, currentSelectedModel) &&
46
- (!preferSessionPreferredModel || preserveCurrentSelectedModelOnSessionChange)
47
- ) {
48
- return currentSelectedModel.trim();
49
- }
50
- if (hasModelOption(modelOptions, selectedSessionPreferredModel)) {
51
- return selectedSessionPreferredModel.trim();
52
- }
53
- if (hasModelOption(modelOptions, fallbackPreferredModel)) {
54
- return fallbackPreferredModel.trim();
55
- }
56
- if (hasModelOption(modelOptions, defaultModel)) {
57
- return defaultModel.trim();
58
- }
59
- return modelOptions[0]?.value ?? '';
60
- }
61
-
62
- export function resolveRecentSessionPreferredModel(params: {
63
- sessions: readonly SessionEntryView[];
64
- selectedSessionKey?: string | null;
65
- sessionType?: string | null;
66
- }): string | undefined {
67
- const targetSessionType = normalizeSessionType(params.sessionType);
68
- let bestSession: SessionEntryView | null = null;
69
- let bestTimestamp = Number.NEGATIVE_INFINITY;
70
- for (const session of params.sessions) {
71
- if (session.key === params.selectedSessionKey) {
72
- continue;
73
- }
74
- if (normalizeSessionType(session.sessionType) !== targetSessionType) {
75
- continue;
76
- }
77
- const preferredModel = session.preferredModel?.trim();
78
- if (!preferredModel) {
79
- continue;
80
- }
81
- const updatedAtTimestamp = Date.parse(session.updatedAt);
82
- const comparableTimestamp = Number.isFinite(updatedAtTimestamp) ? updatedAtTimestamp : Number.NEGATIVE_INFINITY;
83
- if (!bestSession || comparableTimestamp > bestTimestamp) {
84
- bestSession = session;
85
- bestTimestamp = comparableTimestamp;
86
- }
87
- }
88
- return bestSession?.preferredModel?.trim();
89
- }
90
-
91
- export function useSyncSelectedModel(params: {
92
- modelOptions: ChatModelOption[];
93
- selectedSessionKey?: string | null;
94
- selectedSessionExists?: boolean;
95
- selectedSessionPreferredModel?: string;
96
- fallbackPreferredModel?: string;
97
- defaultModel?: string;
98
- setSelectedModel: Dispatch<SetStateAction<string>>;
99
- }) {
100
- const {
101
- modelOptions,
102
- selectedSessionKey,
103
- selectedSessionExists = false,
104
- selectedSessionPreferredModel,
105
- fallbackPreferredModel,
106
- defaultModel,
107
- setSelectedModel
108
- } = params;
109
- const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
110
-
111
- useEffect(() => {
112
- const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
113
- if (modelOptions.length === 0) {
114
- setSelectedModel('');
115
- previousSessionKeyRef.current = selectedSessionKey;
116
- return;
117
- }
118
- setSelectedModel((prev) => {
119
- return resolveSelectedModelValue({
120
- currentSelectedModel: prev,
121
- modelOptions,
122
- selectedSessionPreferredModel,
123
- fallbackPreferredModel,
124
- defaultModel,
125
- preferSessionPreferredModel: sessionChanged,
126
- preserveCurrentSelectedModelOnSessionChange:
127
- sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
128
- });
129
- });
130
- previousSessionKeyRef.current = selectedSessionKey;
131
- }, [
132
- defaultModel,
133
- fallbackPreferredModel,
134
- modelOptions,
135
- selectedSessionExists,
136
- selectedSessionKey,
137
- selectedSessionPreferredModel,
138
- setSelectedModel
139
- ]);
140
- }
141
-
142
8
  export function useSessionRunStatus(params: {
143
9
  view: ChatMainPanelView;
144
10
  selectedSessionKey: string | null;
@@ -0,0 +1,303 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { Dispatch, SetStateAction } from 'react';
3
+ import type { SessionEntryView, ThinkingLevel } from '@/api/types';
4
+ import type { ChatModelOption } from '@/components/chat/chat-input.types';
5
+
6
+ function normalizeSessionType(value: string | null | undefined): string {
7
+ const normalized = value?.trim().toLowerCase();
8
+ return normalized || 'native';
9
+ }
10
+
11
+ function hasModelOption(modelOptions: ChatModelOption[], value: unknown): value is string {
12
+ if (typeof value !== 'string') {
13
+ return false;
14
+ }
15
+ const normalized = value.trim();
16
+ if (!normalized) {
17
+ return false;
18
+ }
19
+ return modelOptions.some((option) => option.value === normalized);
20
+ }
21
+
22
+ function hasThinkingLevelOption(levels: readonly ThinkingLevel[], value: unknown): value is ThinkingLevel {
23
+ return typeof value === 'string' && levels.includes(value as ThinkingLevel);
24
+ }
25
+
26
+ function resolveFallbackThinkingLevel(levels: readonly ThinkingLevel[]): ThinkingLevel | null {
27
+ if (levels.length === 0) {
28
+ return null;
29
+ }
30
+ if (levels.includes('off')) {
31
+ return 'off';
32
+ }
33
+ return levels[0] ?? null;
34
+ }
35
+
36
+ type ResolveSessionPreferenceValueParams<T> = {
37
+ currentValue: unknown;
38
+ selectedSessionPreferredValue?: unknown;
39
+ fallbackPreferredValue?: unknown;
40
+ defaultValue?: unknown;
41
+ isValueSupported: (value: unknown) => value is T;
42
+ firstAvailableValue: T;
43
+ preferSessionPreferredValue?: boolean;
44
+ preserveCurrentValueOnSessionChange?: boolean;
45
+ };
46
+
47
+ export function resolveSessionPreferenceValue<T>(params: ResolveSessionPreferenceValueParams<T>): T {
48
+ const {
49
+ currentValue,
50
+ selectedSessionPreferredValue,
51
+ fallbackPreferredValue,
52
+ defaultValue,
53
+ isValueSupported,
54
+ firstAvailableValue,
55
+ preferSessionPreferredValue = false,
56
+ preserveCurrentValueOnSessionChange = false
57
+ } = params;
58
+ if (isValueSupported(currentValue) && (!preferSessionPreferredValue || preserveCurrentValueOnSessionChange)) {
59
+ return currentValue;
60
+ }
61
+ if (isValueSupported(selectedSessionPreferredValue)) {
62
+ return selectedSessionPreferredValue;
63
+ }
64
+ if (isValueSupported(fallbackPreferredValue)) {
65
+ return fallbackPreferredValue;
66
+ }
67
+ if (isValueSupported(defaultValue)) {
68
+ return defaultValue;
69
+ }
70
+ return firstAvailableValue;
71
+ }
72
+
73
+ export function resolveSelectedModelValue(params: {
74
+ currentSelectedModel?: string;
75
+ modelOptions: ChatModelOption[];
76
+ selectedSessionPreferredModel?: string;
77
+ fallbackPreferredModel?: string;
78
+ defaultModel?: string;
79
+ preferSessionPreferredModel?: boolean;
80
+ preserveCurrentSelectedModelOnSessionChange?: boolean;
81
+ }): string {
82
+ const { modelOptions } = params;
83
+ if (modelOptions.length === 0) {
84
+ return '';
85
+ }
86
+ return resolveSessionPreferenceValue<string>({
87
+ currentValue: params.currentSelectedModel,
88
+ selectedSessionPreferredValue: params.selectedSessionPreferredModel,
89
+ fallbackPreferredValue: params.fallbackPreferredModel,
90
+ defaultValue: params.defaultModel,
91
+ isValueSupported: (value): value is string => hasModelOption(modelOptions, value),
92
+ firstAvailableValue: modelOptions[0]?.value ?? '',
93
+ preferSessionPreferredValue: params.preferSessionPreferredModel,
94
+ preserveCurrentValueOnSessionChange: params.preserveCurrentSelectedModelOnSessionChange
95
+ });
96
+ }
97
+
98
+ export function resolveSelectedThinkingLevelValue(params: {
99
+ currentSelectedThinkingLevel?: ThinkingLevel | null;
100
+ supportedThinkingLevels: readonly ThinkingLevel[];
101
+ selectedSessionPreferredThinking?: ThinkingLevel | null;
102
+ fallbackPreferredThinking?: ThinkingLevel | null;
103
+ defaultThinkingLevel?: ThinkingLevel | null;
104
+ preferSessionPreferredThinking?: boolean;
105
+ preserveCurrentSelectedThinkingOnSessionChange?: boolean;
106
+ }): ThinkingLevel | null {
107
+ const { supportedThinkingLevels } = params;
108
+ if (supportedThinkingLevels.length === 0) {
109
+ return null;
110
+ }
111
+ return resolveSessionPreferenceValue<ThinkingLevel>({
112
+ currentValue: params.currentSelectedThinkingLevel,
113
+ selectedSessionPreferredValue: params.selectedSessionPreferredThinking,
114
+ fallbackPreferredValue: params.fallbackPreferredThinking,
115
+ defaultValue: params.defaultThinkingLevel,
116
+ isValueSupported: (value): value is ThinkingLevel => hasThinkingLevelOption(supportedThinkingLevels, value),
117
+ firstAvailableValue: resolveFallbackThinkingLevel(supportedThinkingLevels) ?? 'off',
118
+ preferSessionPreferredValue: params.preferSessionPreferredThinking,
119
+ preserveCurrentValueOnSessionChange: params.preserveCurrentSelectedThinkingOnSessionChange
120
+ });
121
+ }
122
+
123
+ export function resolveRecentSessionPreferredValue<T>(params: {
124
+ sessions: readonly SessionEntryView[];
125
+ selectedSessionKey?: string | null;
126
+ sessionType?: string | null;
127
+ readPreference: (session: SessionEntryView) => T | null | undefined;
128
+ }): T | undefined {
129
+ const targetSessionType = normalizeSessionType(params.sessionType);
130
+ let bestValue: T | undefined;
131
+ let bestTimestamp = Number.NEGATIVE_INFINITY;
132
+ for (const session of params.sessions) {
133
+ if (session.key === params.selectedSessionKey) {
134
+ continue;
135
+ }
136
+ if (normalizeSessionType(session.sessionType) !== targetSessionType) {
137
+ continue;
138
+ }
139
+ const value = params.readPreference(session);
140
+ if (value === null || value === undefined) {
141
+ continue;
142
+ }
143
+ const updatedAtTimestamp = Date.parse(session.updatedAt);
144
+ const comparableTimestamp = Number.isFinite(updatedAtTimestamp) ? updatedAtTimestamp : Number.NEGATIVE_INFINITY;
145
+ if (bestValue === undefined || comparableTimestamp > bestTimestamp) {
146
+ bestValue = value;
147
+ bestTimestamp = comparableTimestamp;
148
+ }
149
+ }
150
+ return bestValue;
151
+ }
152
+
153
+ export function resolveRecentSessionPreferredModel(params: {
154
+ sessions: readonly SessionEntryView[];
155
+ selectedSessionKey?: string | null;
156
+ sessionType?: string | null;
157
+ }): string | undefined {
158
+ return resolveRecentSessionPreferredValue<string>({
159
+ sessions: params.sessions,
160
+ selectedSessionKey: params.selectedSessionKey,
161
+ sessionType: params.sessionType,
162
+ readPreference: (session) => {
163
+ const preferredModel = session.preferredModel?.trim();
164
+ return preferredModel || undefined;
165
+ }
166
+ });
167
+ }
168
+
169
+ export function resolveRecentSessionPreferredThinking(params: {
170
+ sessions: readonly SessionEntryView[];
171
+ selectedSessionKey?: string | null;
172
+ sessionType?: string | null;
173
+ }): ThinkingLevel | undefined {
174
+ return resolveRecentSessionPreferredValue<ThinkingLevel>({
175
+ sessions: params.sessions,
176
+ selectedSessionKey: params.selectedSessionKey,
177
+ sessionType: params.sessionType,
178
+ readPreference: (session) => session.preferredThinking ?? undefined
179
+ });
180
+ }
181
+
182
+ type UseSyncSessionPreferenceParams<T> = {
183
+ isPreferenceAvailable: boolean;
184
+ emptyValue: T;
185
+ selectedSessionKey?: string | null;
186
+ selectedSessionExists?: boolean;
187
+ setValue: Dispatch<SetStateAction<T>>;
188
+ resolveValue: (params: {
189
+ currentValue: T;
190
+ sessionChanged: boolean;
191
+ preserveCurrentValueOnSessionChange: boolean;
192
+ }) => T;
193
+ };
194
+
195
+ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>) {
196
+ const {
197
+ isPreferenceAvailable,
198
+ emptyValue,
199
+ selectedSessionKey,
200
+ selectedSessionExists = false,
201
+ setValue,
202
+ resolveValue
203
+ } = params;
204
+ const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
205
+ const resolveValueRef = useRef(resolveValue);
206
+
207
+ useEffect(() => {
208
+ resolveValueRef.current = resolveValue;
209
+ }, [resolveValue]);
210
+
211
+ useEffect(() => {
212
+ const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
213
+ if (!isPreferenceAvailable) {
214
+ setValue(emptyValue);
215
+ previousSessionKeyRef.current = selectedSessionKey;
216
+ return;
217
+ }
218
+ setValue((prev) =>
219
+ resolveValueRef.current({
220
+ currentValue: prev,
221
+ sessionChanged,
222
+ preserveCurrentValueOnSessionChange: sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
223
+ })
224
+ );
225
+ previousSessionKeyRef.current = selectedSessionKey;
226
+ }, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue]);
227
+ }
228
+
229
+ export function useSyncSelectedModel(params: {
230
+ modelOptions: ChatModelOption[];
231
+ selectedSessionKey?: string | null;
232
+ selectedSessionExists?: boolean;
233
+ selectedSessionPreferredModel?: string;
234
+ fallbackPreferredModel?: string;
235
+ defaultModel?: string;
236
+ setSelectedModel: Dispatch<SetStateAction<string>>;
237
+ }) {
238
+ const {
239
+ modelOptions,
240
+ selectedSessionKey,
241
+ selectedSessionExists = false,
242
+ selectedSessionPreferredModel,
243
+ fallbackPreferredModel,
244
+ defaultModel,
245
+ setSelectedModel
246
+ } = params;
247
+
248
+ useSyncSessionPreference<string>({
249
+ isPreferenceAvailable: modelOptions.length > 0,
250
+ emptyValue: '',
251
+ selectedSessionKey,
252
+ selectedSessionExists,
253
+ setValue: setSelectedModel,
254
+ resolveValue: ({ currentValue, sessionChanged, preserveCurrentValueOnSessionChange }) =>
255
+ resolveSelectedModelValue({
256
+ currentSelectedModel: currentValue,
257
+ modelOptions,
258
+ selectedSessionPreferredModel,
259
+ fallbackPreferredModel,
260
+ defaultModel,
261
+ preferSessionPreferredModel: sessionChanged,
262
+ preserveCurrentSelectedModelOnSessionChange: preserveCurrentValueOnSessionChange
263
+ })
264
+ });
265
+ }
266
+
267
+ export function useSyncSelectedThinking(params: {
268
+ supportedThinkingLevels: readonly ThinkingLevel[];
269
+ selectedSessionKey?: string | null;
270
+ selectedSessionExists?: boolean;
271
+ selectedSessionPreferredThinking?: ThinkingLevel | null;
272
+ fallbackPreferredThinking?: ThinkingLevel | null;
273
+ defaultThinkingLevel?: ThinkingLevel | null;
274
+ setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
275
+ }) {
276
+ const {
277
+ supportedThinkingLevels,
278
+ selectedSessionKey,
279
+ selectedSessionExists = false,
280
+ selectedSessionPreferredThinking,
281
+ fallbackPreferredThinking,
282
+ defaultThinkingLevel,
283
+ setSelectedThinkingLevel
284
+ } = params;
285
+
286
+ useSyncSessionPreference<ThinkingLevel | null>({
287
+ isPreferenceAvailable: supportedThinkingLevels.length > 0,
288
+ emptyValue: null,
289
+ selectedSessionKey,
290
+ selectedSessionExists,
291
+ setValue: setSelectedThinkingLevel,
292
+ resolveValue: ({ currentValue, sessionChanged, preserveCurrentValueOnSessionChange }) =>
293
+ resolveSelectedThinkingLevelValue({
294
+ currentSelectedThinkingLevel: currentValue,
295
+ supportedThinkingLevels,
296
+ selectedSessionPreferredThinking,
297
+ fallbackPreferredThinking,
298
+ defaultThinkingLevel,
299
+ preferSessionPreferredThinking: sessionChanged,
300
+ preserveCurrentSelectedThinkingOnSessionChange: preserveCurrentValueOnSessionChange
301
+ })
302
+ });
303
+ }