@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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/ChannelsList-DDfZIiJa.js +1 -0
- package/dist/assets/ChatPage-FpRraTxm.js +38 -0
- package/dist/assets/{DocBrowser-CVwUDJMO.js → DocBrowser-Kndx8OJj.js} +1 -1
- package/dist/assets/LogoBadge-hKHoLH9n.js +1 -0
- package/dist/assets/MarketplacePage-CZIJyfjK.js +49 -0
- package/dist/assets/McpMarketplacePage-BGrAMA37.js +40 -0
- package/dist/assets/{ModelConfig-CsX-_fyy.js → ModelConfig-BpKQeGfb.js} +1 -1
- package/dist/assets/ProvidersList-qfUL6mrW.js +1 -0
- package/dist/assets/RemoteAccessPage-BQuMsngI.js +1 -0
- package/dist/assets/{RuntimeConfig-CX2TGEG1.js → RuntimeConfig-CVlqNWKO.js} +1 -1
- package/dist/assets/{SearchConfig-C-WBTcWi.js → SearchConfig-DXFV6Mvx.js} +1 -1
- package/dist/assets/{SecretsConfig-9kbR0ZCB.js → SecretsConfig-BGW9aUqv.js} +2 -2
- package/dist/assets/{SessionsConfig-Bohn3P1q.js → SessionsConfig-BByfa1ke.js} +2 -2
- package/dist/assets/{chat-message-AWIcksDK.js → chat-message-ZwnDwDuQ.js} +1 -1
- package/dist/assets/index-BWvap_iq.js +8 -0
- package/dist/assets/index-COrhpAdh.css +1 -0
- package/dist/assets/{index-CPDASUXh.js → index-Ct7FQpxN.js} +1 -1
- package/dist/assets/{label-DD61y-4v.js → label-Bklr3fXc.js} +1 -1
- package/dist/assets/marketplace-localization-Dk31LJJJ.js +1 -0
- package/dist/assets/{page-layout-CfnoVycc.js → page-layout-sNhcbwtm.js} +1 -1
- package/dist/assets/{popover-DsugZ6rp.js → popover-C3rJrJJG.js} +1 -1
- package/dist/assets/{security-config-DIrf2Z0O.js → security-config-BueosYw1.js} +1 -1
- package/dist/assets/skeleton-CiG6msbm.js +1 -0
- package/dist/assets/status-dot-CsIV5YrS.js +1 -0
- package/dist/assets/{switch-NX5OmUXQ.js → switch-DSdHSIsC.js} +1 -1
- package/dist/assets/{tabs-custom-9ihB5Jem.js → tabs-custom-BB-VjdL2.js} +1 -1
- package/dist/assets/{useConfirmDialog-BuQnVTeR.js → useConfirmDialog-BL5s8KDC.js} +2 -2
- package/dist/assets/{vendor-DKBNiC31.js → vendor-CwsIoNvJ.js} +128 -93
- package/dist/index.html +3 -3
- package/package.json +3 -3
- package/src/App.tsx +4 -0
- package/src/api/auth.types.ts +24 -0
- package/src/api/chat-session-type.types.ts +21 -0
- package/src/api/marketplace.ts +8 -2
- package/src/api/mcp-marketplace.ts +138 -0
- package/src/api/remote.ts +77 -0
- package/src/api/remote.types.ts +104 -0
- package/src/api/types.ts +28 -34
- package/src/components/chat/ChatSidebar.test.tsx +31 -2
- package/src/components/chat/ChatSidebar.tsx +26 -2
- package/src/components/chat/chat-page-data.ts +36 -38
- package/src/components/chat/chat-page-runtime.test.ts +96 -2
- package/src/components/chat/chat-page-runtime.ts +1 -135
- package/src/components/chat/chat-session-preference-governance.ts +303 -0
- package/src/components/chat/legacy/LegacyChatPage.tsx +4 -19
- package/src/components/chat/ncp/NcpChatPage.tsx +4 -19
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +36 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +62 -21
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +2 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +2 -0
- package/src/components/chat/stores/chat-input.store.ts +14 -1
- package/src/components/chat/useChatSessionTypeState.test.tsx +29 -0
- package/src/components/chat/useChatSessionTypeState.ts +55 -12
- package/src/components/layout/Sidebar.tsx +11 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +152 -0
- package/src/components/marketplace/MarketplacePage.tsx +52 -199
- package/src/components/marketplace/marketplace-installed-cache.test.ts +110 -0
- package/src/components/marketplace/marketplace-installed-cache.ts +149 -0
- package/src/components/marketplace/marketplace-localization.ts +77 -0
- package/src/components/marketplace/marketplace-page-parts.tsx +102 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +208 -0
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +578 -0
- package/src/components/remote/RemoteAccessPage.tsx +396 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/hooks/useMarketplace.ts +36 -7
- package/src/hooks/useMcpMarketplace.ts +99 -0
- package/src/hooks/useRemoteAccess.ts +120 -0
- package/src/hooks/useWebSocket.ts +25 -16
- package/src/lib/i18n.marketplace.ts +91 -0
- package/src/lib/i18n.remote.ts +142 -0
- package/src/lib/i18n.ts +10 -68
- package/dist/assets/ChannelsList-DKD6Llid.js +0 -1
- package/dist/assets/ChatPage-BK9X4Tin.js +0 -38
- package/dist/assets/LogoBadge-CYQ_b7jk.js +0 -1
- package/dist/assets/MarketplacePage-B_2z3ii_.js +0 -49
- package/dist/assets/ProvidersList-CZstsyv7.js +0 -1
- package/dist/assets/index-BEgClaDH.js +0 -8
- package/dist/assets/index-C8GsgIUn.css +0 -1
- 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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
+
}
|