@nextclaw/ui 0.9.1 → 0.9.3

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-ZBPiF0y2.js +1 -0
  3. package/dist/assets/ChatPage-BOgoolWK.js +38 -0
  4. package/dist/assets/{DocBrowser-LpzGe8An.js → DocBrowser-BUYNHg0Y.js} +1 -1
  5. package/dist/assets/LogoBadge-DXPq99LJ.js +1 -0
  6. package/dist/assets/MarketplacePage-Dx7nexYN.js +49 -0
  7. package/dist/assets/McpMarketplacePage-064wdotP.js +40 -0
  8. package/dist/assets/{ModelConfig-DuImUHIX.js → ModelConfig-BDIfLesG.js} +1 -1
  9. package/dist/assets/ProvidersList-DrlIr46m.js +1 -0
  10. package/dist/assets/RemoteAccessPage-ZkUBA-Av.js +1 -0
  11. package/dist/assets/{RuntimeConfig-C6iqpJR_.js → RuntimeConfig-BPxXEGzM.js} +1 -1
  12. package/dist/assets/{SearchConfig-Dvp1TAXu.js → SearchConfig-BIqnlpne.js} +1 -1
  13. package/dist/assets/{SecretsConfig-D5Ymlvt9.js → SecretsConfig-jKZEVF2q.js} +2 -2
  14. package/dist/assets/{SessionsConfig-CIA_jA1P.js → SessionsConfig-C_FXgVe1.js} +2 -2
  15. package/dist/assets/{chat-message-B60Fh9kI.js → chat-message-DmzpZJc_.js} +1 -1
  16. package/dist/assets/index-Byfw276e.js +8 -0
  17. package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
  18. package/dist/assets/index-bhNuQis7.css +1 -0
  19. package/dist/assets/{label-D4fGx6Wb.js → label-B1MloEtn.js} +1 -1
  20. package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
  21. package/dist/assets/{page-layout-twy8gmBE.js → page-layout-BGg1EhM5.js} +1 -1
  22. package/dist/assets/{popover-DYbYpt1j.js → popover-jJMv74Fp.js} +1 -1
  23. package/dist/assets/{security-config-BcIZ4rpb.js → security-config-Boh9NIMz.js} +1 -1
  24. package/dist/assets/skeleton-CmATs_b3.js +1 -0
  25. package/dist/assets/status-dot-DNyCdxPZ.js +1 -0
  26. package/dist/assets/{switch-DqA6r5XR.js → switch-DE_MYk7x.js} +1 -1
  27. package/dist/assets/{tabs-custom-C6enKKs1.js → tabs-custom-B-zErYPr.js} +1 -1
  28. package/dist/assets/{useConfirmDialog-CHBf5Of7.js → useConfirmDialog-BqQ6QfhB.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 +4 -4
  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 +57 -0
  38. package/src/api/remote.types.ts +80 -0
  39. package/src/api/types.ts +91 -37
  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 +37 -53
  43. package/src/components/chat/chat-page-runtime.test.ts +122 -2
  44. package/src/components/chat/chat-page-runtime.ts +1 -118
  45. package/src/components/chat/chat-session-preference-governance.ts +303 -0
  46. package/src/components/chat/legacy/LegacyChatPage.tsx +4 -34
  47. package/src/components/chat/ncp/NcpChatPage.tsx +4 -34
  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 +63 -36
  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 +320 -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 +92 -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 +115 -0
  73. package/src/lib/i18n.ts +10 -68
  74. package/dist/assets/ChannelsList-DhvjpZcs.js +0 -1
  75. package/dist/assets/ChatPage-B8VBaMQm.js +0 -38
  76. package/dist/assets/LogoBadge-Be4lktJN.js +0 -1
  77. package/dist/assets/MarketplacePage-Cx9AI3_h.js +0 -49
  78. package/dist/assets/ProvidersList-Ccleg25k.js +0 -1
  79. package/dist/assets/index-BiPDnzv0.js +0 -8
  80. package/dist/assets/index-C8GsgIUn.css +0 -1
  81. package/dist/assets/skeleton-DypBy7jp.js +0 -1
@@ -85,6 +85,16 @@ function resolveSessionTypeLabel(
85
85
  .join(' ');
86
86
  }
87
87
 
88
+ function resolveSessionTypeStatusText(option: {
89
+ ready?: boolean;
90
+ reasonMessage?: string | null;
91
+ }): string {
92
+ if (option.ready === false) {
93
+ return option.reasonMessage?.trim() || t('statusSetup');
94
+ }
95
+ return t('statusReady');
96
+ }
97
+
88
98
  const navItems = [
89
99
  { target: '/cron', label: () => t('chatSidebarScheduledTasks'), icon: AlarmClock },
90
100
  { target: '/skills', label: () => t('chatSidebarSkills'), icon: BrainCircuit },
@@ -168,8 +178,22 @@ export function ChatSidebar() {
168
178
  }}
169
179
  className="w-full rounded-xl px-3 py-2 text-left transition-colors hover:bg-gray-100"
170
180
  >
171
- <div className="text-[13px] font-medium text-gray-900">{option.label}</div>
172
- <div className="mt-0.5 text-[11px] text-gray-500">{t('chatSidebarNewTask')}</div>
181
+ <div className="flex items-center justify-between gap-3">
182
+ <div className="text-[13px] font-medium text-gray-900">{option.label}</div>
183
+ <span
184
+ className={cn(
185
+ 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold',
186
+ option.ready === false
187
+ ? 'bg-amber-100 text-amber-800'
188
+ : 'bg-emerald-100 text-emerald-700'
189
+ )}
190
+ >
191
+ {option.ready === false ? t('statusSetup') : t('statusReady')}
192
+ </span>
193
+ </div>
194
+ <div className="mt-0.5 text-[11px] text-gray-500">
195
+ {resolveSessionTypeStatusText(option)}
196
+ </div>
173
197
  </button>
174
198
  ))}
175
199
  </div>
@@ -4,10 +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
- resolveSelectedModelValue,
7
+ resolveRecentSessionPreferredThinking,
8
8
  resolveRecentSessionPreferredModel,
9
- useSyncSelectedModel
10
- } from '@/components/chat/chat-page-runtime';
9
+ useSyncSelectedModel,
10
+ useSyncSelectedThinking
11
+ } from '@/components/chat/chat-session-preference-governance';
11
12
  import {
12
13
  useChatCapabilities,
13
14
  useChatSessionTypes,
@@ -23,24 +24,13 @@ type UseChatPageDataParams = {
23
24
  query: string;
24
25
  selectedSessionKey: string | null;
25
26
  selectedAgentId: string;
27
+ currentSelectedModel: string;
26
28
  pendingSessionType: string;
27
29
  setPendingSessionType: Dispatch<SetStateAction<string>>;
28
30
  setSelectedModel: Dispatch<SetStateAction<string>>;
31
+ setSelectedThinkingLevel: Dispatch<SetStateAction<ThinkingLevel | null>>;
29
32
  };
30
33
 
31
- const THINKING_LEVEL_SET = new Set<string>(['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh']);
32
-
33
- function parseThinkingLevel(value: unknown): ThinkingLevel | null {
34
- if (typeof value !== 'string') {
35
- return null;
36
- }
37
- const normalized = value.trim().toLowerCase();
38
- if (!normalized) {
39
- return null;
40
- }
41
- return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
42
- }
43
-
44
34
  export function useChatPageData(params: UseChatPageDataParams) {
45
35
  const configQuery = useConfig();
46
36
  const configMetaQuery = useConfigMeta();
@@ -111,52 +101,48 @@ export function useChatPageData(params: UseChatPageDataParams) {
111
101
  }),
112
102
  [params.selectedSessionKey, sessionTypeState.selectedSessionType, sessions]
113
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
+ );
114
125
 
115
126
  useSyncSelectedModel({
116
127
  modelOptions,
117
128
  selectedSessionKey: params.selectedSessionKey,
129
+ selectedSessionExists: Boolean(selectedSession),
118
130
  selectedSessionPreferredModel: selectedSession?.preferredModel,
119
131
  fallbackPreferredModel: recentSessionPreferredModel,
120
132
  defaultModel: configQuery.data?.agents.defaults.model,
121
133
  setSelectedModel: params.setSelectedModel
122
134
  });
123
-
124
- const hydratedSessionModel = useMemo(
125
- () =>
126
- resolveSelectedModelValue({
127
- currentSelectedModel: '',
128
- modelOptions,
129
- selectedSessionPreferredModel: selectedSession?.preferredModel,
130
- fallbackPreferredModel: recentSessionPreferredModel,
131
- defaultModel: configQuery.data?.agents.defaults.model,
132
- preferSessionPreferredModel: true
133
- }),
134
- [configQuery.data?.agents.defaults.model, modelOptions, recentSessionPreferredModel, selectedSession?.preferredModel]
135
- );
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
+ });
136
144
 
137
145
  const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
138
- const selectedSessionThinkingLevel = useMemo(() => {
139
- if (!params.selectedSessionKey) {
140
- return null;
141
- }
142
- const metadata = historyQuery.data?.metadata;
143
- if (!metadata || typeof metadata !== 'object') {
144
- return null;
145
- }
146
- const candidates = [
147
- metadata.preferred_thinking,
148
- metadata.thinking,
149
- metadata.thinking_level,
150
- metadata.thinkingLevel
151
- ];
152
- for (const value of candidates) {
153
- const level = parseThinkingLevel(value);
154
- if (level) {
155
- return level;
156
- }
157
- }
158
- return null;
159
- }, [historyQuery.data?.metadata, params.selectedSessionKey]);
160
146
 
161
147
  return {
162
148
  configQuery,
@@ -171,9 +157,7 @@ export function useChatPageData(params: UseChatPageDataParams) {
171
157
  sessions,
172
158
  skillRecords,
173
159
  selectedSession,
174
- hydratedSessionModel,
175
160
  historyMessages,
176
- selectedSessionThinkingLevel,
177
161
  ...sessionTypeState
178
162
  };
179
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(
@@ -82,6 +92,32 @@ describe('resolveSelectedModelValue', () => {
82
92
  ).toBe('openai/gpt-5');
83
93
  });
84
94
 
95
+ it('preserves the current valid model when a draft session materializes before the new session metadata exists', () => {
96
+ expect(
97
+ resolveSelectedModelValue({
98
+ currentSelectedModel: 'openai/gpt-5',
99
+ modelOptions,
100
+ fallbackPreferredModel: 'anthropic/claude-sonnet-4',
101
+ defaultModel: 'anthropic/claude-sonnet-4',
102
+ preferSessionPreferredModel: true,
103
+ preserveCurrentSelectedModelOnSessionChange: true
104
+ })
105
+ ).toBe('openai/gpt-5');
106
+ });
107
+
108
+ it('still falls back when the current model is no longer valid during draft session materialization', () => {
109
+ expect(
110
+ resolveSelectedModelValue({
111
+ currentSelectedModel: 'missing/model',
112
+ modelOptions,
113
+ fallbackPreferredModel: 'openai/gpt-5',
114
+ defaultModel: 'anthropic/claude-sonnet-4',
115
+ preferSessionPreferredModel: true,
116
+ preserveCurrentSelectedModelOnSessionChange: true
117
+ })
118
+ ).toBe('openai/gpt-5');
119
+ });
120
+
85
121
  it('uses the recent same-runtime model when the current session has no valid preferred model', () => {
86
122
  expect(
87
123
  resolveSelectedModelValue({
@@ -179,3 +215,87 @@ describe('resolveRecentSessionPreferredModel', () => {
179
215
  ).toBe('anthropic/claude-sonnet-4');
180
216
  });
181
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,127 +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
- }): string {
31
- const {
32
- currentSelectedModel,
33
- modelOptions,
34
- selectedSessionPreferredModel,
35
- fallbackPreferredModel,
36
- defaultModel,
37
- preferSessionPreferredModel = false
38
- } = params;
39
- if (modelOptions.length === 0) {
40
- return '';
41
- }
42
- if (!preferSessionPreferredModel && hasModelOption(modelOptions, currentSelectedModel)) {
43
- return currentSelectedModel.trim();
44
- }
45
- if (hasModelOption(modelOptions, selectedSessionPreferredModel)) {
46
- return selectedSessionPreferredModel.trim();
47
- }
48
- if (hasModelOption(modelOptions, fallbackPreferredModel)) {
49
- return fallbackPreferredModel.trim();
50
- }
51
- if (hasModelOption(modelOptions, defaultModel)) {
52
- return defaultModel.trim();
53
- }
54
- return modelOptions[0]?.value ?? '';
55
- }
56
-
57
- export function resolveRecentSessionPreferredModel(params: {
58
- sessions: readonly SessionEntryView[];
59
- selectedSessionKey?: string | null;
60
- sessionType?: string | null;
61
- }): string | undefined {
62
- const targetSessionType = normalizeSessionType(params.sessionType);
63
- let bestSession: SessionEntryView | null = null;
64
- let bestTimestamp = Number.NEGATIVE_INFINITY;
65
- for (const session of params.sessions) {
66
- if (session.key === params.selectedSessionKey) {
67
- continue;
68
- }
69
- if (normalizeSessionType(session.sessionType) !== targetSessionType) {
70
- continue;
71
- }
72
- const preferredModel = session.preferredModel?.trim();
73
- if (!preferredModel) {
74
- continue;
75
- }
76
- const updatedAtTimestamp = Date.parse(session.updatedAt);
77
- const comparableTimestamp = Number.isFinite(updatedAtTimestamp) ? updatedAtTimestamp : Number.NEGATIVE_INFINITY;
78
- if (!bestSession || comparableTimestamp > bestTimestamp) {
79
- bestSession = session;
80
- bestTimestamp = comparableTimestamp;
81
- }
82
- }
83
- return bestSession?.preferredModel?.trim();
84
- }
85
-
86
- export function useSyncSelectedModel(params: {
87
- modelOptions: ChatModelOption[];
88
- selectedSessionKey?: string | null;
89
- selectedSessionPreferredModel?: string;
90
- fallbackPreferredModel?: string;
91
- defaultModel?: string;
92
- setSelectedModel: Dispatch<SetStateAction<string>>;
93
- }) {
94
- const {
95
- modelOptions,
96
- selectedSessionKey,
97
- selectedSessionPreferredModel,
98
- fallbackPreferredModel,
99
- defaultModel,
100
- setSelectedModel
101
- } = params;
102
- const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
103
-
104
- useEffect(() => {
105
- const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
106
- if (modelOptions.length === 0) {
107
- setSelectedModel('');
108
- previousSessionKeyRef.current = selectedSessionKey;
109
- return;
110
- }
111
- setSelectedModel((prev) => {
112
- return resolveSelectedModelValue({
113
- currentSelectedModel: prev,
114
- modelOptions,
115
- selectedSessionPreferredModel,
116
- fallbackPreferredModel,
117
- defaultModel,
118
- preferSessionPreferredModel: sessionChanged
119
- });
120
- });
121
- previousSessionKeyRef.current = selectedSessionKey;
122
- }, [defaultModel, fallbackPreferredModel, modelOptions, selectedSessionKey, selectedSessionPreferredModel, setSelectedModel]);
123
- }
124
-
125
8
  export function useSessionRunStatus(params: {
126
9
  view: ChatMainPanelView;
127
10
  selectedSessionKey: string | null;