@nextclaw/ui 0.9.0 → 0.9.2
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 +14 -0
- package/dist/assets/{ChannelsList-C7F_As4r.js → ChannelsList-DKD6Llid.js} +1 -1
- package/dist/assets/ChatPage-BK9X4Tin.js +38 -0
- package/dist/assets/{DocBrowser-Dsd8Dlq8.js → DocBrowser-CVwUDJMO.js} +1 -1
- package/dist/assets/{LogoBadge-2ChEc_oz.js → LogoBadge-CYQ_b7jk.js} +1 -1
- package/dist/assets/{MarketplacePage-BXck6-X3.js → MarketplacePage-B_2z3ii_.js} +1 -1
- package/dist/assets/{ModelConfig-CgHRSD0b.js → ModelConfig-CsX-_fyy.js} +1 -1
- package/dist/assets/{ProvidersList-PPfZucvS.js → ProvidersList-CZstsyv7.js} +1 -1
- package/dist/assets/{RuntimeConfig-ClLEKNTN.js → RuntimeConfig-CX2TGEG1.js} +1 -1
- package/dist/assets/{SearchConfig-CuXVCbrf.js → SearchConfig-C-WBTcWi.js} +1 -1
- package/dist/assets/{SecretsConfig-udJz6Ake.js → SecretsConfig-9kbR0ZCB.js} +1 -1
- package/dist/assets/{SessionsConfig-C1XnFfiC.js → SessionsConfig-Bohn3P1q.js} +1 -1
- package/dist/assets/{chat-message-BETwXLD4.js → chat-message-AWIcksDK.js} +1 -1
- package/dist/assets/{index-CsvP4CER.js → index-BEgClaDH.js} +3 -3
- package/dist/assets/index-C8GsgIUn.css +1 -0
- package/dist/assets/{index-COJdlL0e.js → index-CPDASUXh.js} +1 -1
- package/dist/assets/{label-BGL-ztxh.js → label-DD61y-4v.js} +1 -1
- package/dist/assets/{page-layout-aw88k7tG.js → page-layout-CfnoVycc.js} +1 -1
- package/dist/assets/{popover-DyEvzhmV.js → popover-DsugZ6rp.js} +1 -1
- package/dist/assets/{security-config-BuPAQn82.js → security-config-DIrf2Z0O.js} +1 -1
- package/dist/assets/skeleton-DJ-Wen2o.js +1 -0
- package/dist/assets/{switch-BK8jIzto.js → switch-NX5OmUXQ.js} +1 -1
- package/dist/assets/{tabs-custom-Da3cEOji.js → tabs-custom-9ihB5Jem.js} +1 -1
- package/dist/assets/{useConfirmDialog-z0CE92iS.js → useConfirmDialog-BuQnVTeR.js} +1 -1
- package/dist/assets/{vendor-CkJHmX1g.js → vendor-DKBNiC31.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +6 -6
- package/src/api/types.ts +63 -3
- package/src/components/chat/chat-composer-state.ts +53 -0
- package/src/components/chat/chat-page-data.ts +1 -15
- package/src/components/chat/chat-page-runtime.test.ts +26 -0
- package/src/components/chat/chat-page-runtime.ts +21 -4
- package/src/components/chat/chat-stream/types.ts +3 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +12 -41
- package/src/components/chat/legacy/LegacyChatPage.tsx +1 -15
- package/src/components/chat/managers/chat-input.manager.ts +43 -13
- package/src/components/chat/ncp/NcpChatPage.tsx +11 -18
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +42 -12
- package/src/components/chat/ncp/ncp-chat-page-data.ts +1 -15
- package/src/components/chat/presenter/chat-presenter-context.tsx +2 -0
- package/src/components/chat/stores/chat-input.store.ts +4 -0
- package/dist/assets/ChatPage-Oo7-OUsx.js +0 -37
- package/dist/assets/index-D-bXl7qL.css +0 -1
- package/dist/assets/skeleton-drzO_tdU.js +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import {
|
|
3
|
+
createChatComposerTokenNode,
|
|
4
|
+
createChatComposerNodesFromText,
|
|
5
|
+
createEmptyChatComposerNodes,
|
|
6
|
+
extractChatComposerTokenKeys,
|
|
7
|
+
normalizeChatComposerNodes,
|
|
8
|
+
removeChatComposerTokenNodes,
|
|
9
|
+
serializeChatComposerPlainText
|
|
10
|
+
} from '@nextclaw/agent-chat-ui';
|
|
11
|
+
|
|
12
|
+
export function createInitialChatComposerNodes(): ChatComposerNode[] {
|
|
13
|
+
return createEmptyChatComposerNodes();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createChatComposerNodesFromDraft(text: string): ChatComposerNode[] {
|
|
17
|
+
return createChatComposerNodesFromText(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function deriveChatComposerDraft(nodes: ChatComposerNode[]): string {
|
|
21
|
+
return serializeChatComposerPlainText(nodes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deriveSelectedSkillsFromComposer(nodes: ChatComposerNode[]): string[] {
|
|
25
|
+
return extractChatComposerTokenKeys(nodes, 'skill');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function syncComposerSkills(
|
|
29
|
+
nodes: ChatComposerNode[],
|
|
30
|
+
nextSkills: string[],
|
|
31
|
+
skillRecords: Array<{ spec: string; label?: string }>
|
|
32
|
+
): ChatComposerNode[] {
|
|
33
|
+
const nextSkillSet = new Set(nextSkills);
|
|
34
|
+
const prunedNodes = removeChatComposerTokenNodes(
|
|
35
|
+
nodes,
|
|
36
|
+
(node) => node.tokenKind === 'skill' && !nextSkillSet.has(node.tokenKey)
|
|
37
|
+
);
|
|
38
|
+
const existingSkills = extractChatComposerTokenKeys(prunedNodes, 'skill');
|
|
39
|
+
const recordMap = new Map(skillRecords.map((record) => [record.spec, record]));
|
|
40
|
+
const appendedNodes = nextSkills
|
|
41
|
+
.filter((skill) => !existingSkills.includes(skill))
|
|
42
|
+
.map((skill) =>
|
|
43
|
+
createChatComposerTokenNode({
|
|
44
|
+
tokenKind: 'skill',
|
|
45
|
+
tokenKey: skill,
|
|
46
|
+
label: recordMap.get(skill)?.label || skill
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return appendedNodes.length === 0
|
|
51
|
+
? prunedNodes
|
|
52
|
+
: normalizeChatComposerNodes([...prunedNodes, ...appendedNodes]);
|
|
53
|
+
}
|
|
@@ -4,7 +4,6 @@ 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,
|
|
8
7
|
resolveRecentSessionPreferredModel,
|
|
9
8
|
useSyncSelectedModel
|
|
10
9
|
} from '@/components/chat/chat-page-runtime';
|
|
@@ -115,25 +114,13 @@ export function useChatPageData(params: UseChatPageDataParams) {
|
|
|
115
114
|
useSyncSelectedModel({
|
|
116
115
|
modelOptions,
|
|
117
116
|
selectedSessionKey: params.selectedSessionKey,
|
|
117
|
+
selectedSessionExists: Boolean(selectedSession),
|
|
118
118
|
selectedSessionPreferredModel: selectedSession?.preferredModel,
|
|
119
119
|
fallbackPreferredModel: recentSessionPreferredModel,
|
|
120
120
|
defaultModel: configQuery.data?.agents.defaults.model,
|
|
121
121
|
setSelectedModel: params.setSelectedModel
|
|
122
122
|
});
|
|
123
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
|
-
);
|
|
136
|
-
|
|
137
124
|
const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
|
|
138
125
|
const selectedSessionThinkingLevel = useMemo(() => {
|
|
139
126
|
if (!params.selectedSessionKey) {
|
|
@@ -171,7 +158,6 @@ export function useChatPageData(params: UseChatPageDataParams) {
|
|
|
171
158
|
sessions,
|
|
172
159
|
skillRecords,
|
|
173
160
|
selectedSession,
|
|
174
|
-
hydratedSessionModel,
|
|
175
161
|
historyMessages,
|
|
176
162
|
selectedSessionThinkingLevel,
|
|
177
163
|
...sessionTypeState
|
|
@@ -82,6 +82,32 @@ describe('resolveSelectedModelValue', () => {
|
|
|
82
82
|
).toBe('openai/gpt-5');
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
it('preserves the current valid model when a draft session materializes before the new session metadata exists', () => {
|
|
86
|
+
expect(
|
|
87
|
+
resolveSelectedModelValue({
|
|
88
|
+
currentSelectedModel: 'openai/gpt-5',
|
|
89
|
+
modelOptions,
|
|
90
|
+
fallbackPreferredModel: 'anthropic/claude-sonnet-4',
|
|
91
|
+
defaultModel: 'anthropic/claude-sonnet-4',
|
|
92
|
+
preferSessionPreferredModel: true,
|
|
93
|
+
preserveCurrentSelectedModelOnSessionChange: true
|
|
94
|
+
})
|
|
95
|
+
).toBe('openai/gpt-5');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('still falls back when the current model is no longer valid during draft session materialization', () => {
|
|
99
|
+
expect(
|
|
100
|
+
resolveSelectedModelValue({
|
|
101
|
+
currentSelectedModel: 'missing/model',
|
|
102
|
+
modelOptions,
|
|
103
|
+
fallbackPreferredModel: 'openai/gpt-5',
|
|
104
|
+
defaultModel: 'anthropic/claude-sonnet-4',
|
|
105
|
+
preferSessionPreferredModel: true,
|
|
106
|
+
preserveCurrentSelectedModelOnSessionChange: true
|
|
107
|
+
})
|
|
108
|
+
).toBe('openai/gpt-5');
|
|
109
|
+
});
|
|
110
|
+
|
|
85
111
|
it('uses the recent same-runtime model when the current session has no valid preferred model', () => {
|
|
86
112
|
expect(
|
|
87
113
|
resolveSelectedModelValue({
|
|
@@ -27,6 +27,7 @@ export function resolveSelectedModelValue(params: {
|
|
|
27
27
|
fallbackPreferredModel?: string;
|
|
28
28
|
defaultModel?: string;
|
|
29
29
|
preferSessionPreferredModel?: boolean;
|
|
30
|
+
preserveCurrentSelectedModelOnSessionChange?: boolean;
|
|
30
31
|
}): string {
|
|
31
32
|
const {
|
|
32
33
|
currentSelectedModel,
|
|
@@ -34,12 +35,16 @@ export function resolveSelectedModelValue(params: {
|
|
|
34
35
|
selectedSessionPreferredModel,
|
|
35
36
|
fallbackPreferredModel,
|
|
36
37
|
defaultModel,
|
|
37
|
-
preferSessionPreferredModel = false
|
|
38
|
+
preferSessionPreferredModel = false,
|
|
39
|
+
preserveCurrentSelectedModelOnSessionChange = false
|
|
38
40
|
} = params;
|
|
39
41
|
if (modelOptions.length === 0) {
|
|
40
42
|
return '';
|
|
41
43
|
}
|
|
42
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
hasModelOption(modelOptions, currentSelectedModel) &&
|
|
46
|
+
(!preferSessionPreferredModel || preserveCurrentSelectedModelOnSessionChange)
|
|
47
|
+
) {
|
|
43
48
|
return currentSelectedModel.trim();
|
|
44
49
|
}
|
|
45
50
|
if (hasModelOption(modelOptions, selectedSessionPreferredModel)) {
|
|
@@ -86,6 +91,7 @@ export function resolveRecentSessionPreferredModel(params: {
|
|
|
86
91
|
export function useSyncSelectedModel(params: {
|
|
87
92
|
modelOptions: ChatModelOption[];
|
|
88
93
|
selectedSessionKey?: string | null;
|
|
94
|
+
selectedSessionExists?: boolean;
|
|
89
95
|
selectedSessionPreferredModel?: string;
|
|
90
96
|
fallbackPreferredModel?: string;
|
|
91
97
|
defaultModel?: string;
|
|
@@ -94,6 +100,7 @@ export function useSyncSelectedModel(params: {
|
|
|
94
100
|
const {
|
|
95
101
|
modelOptions,
|
|
96
102
|
selectedSessionKey,
|
|
103
|
+
selectedSessionExists = false,
|
|
97
104
|
selectedSessionPreferredModel,
|
|
98
105
|
fallbackPreferredModel,
|
|
99
106
|
defaultModel,
|
|
@@ -115,11 +122,21 @@ export function useSyncSelectedModel(params: {
|
|
|
115
122
|
selectedSessionPreferredModel,
|
|
116
123
|
fallbackPreferredModel,
|
|
117
124
|
defaultModel,
|
|
118
|
-
preferSessionPreferredModel: sessionChanged
|
|
125
|
+
preferSessionPreferredModel: sessionChanged,
|
|
126
|
+
preserveCurrentSelectedModelOnSessionChange:
|
|
127
|
+
sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
|
|
119
128
|
});
|
|
120
129
|
});
|
|
121
130
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
122
|
-
}, [
|
|
131
|
+
}, [
|
|
132
|
+
defaultModel,
|
|
133
|
+
fallbackPreferredModel,
|
|
134
|
+
modelOptions,
|
|
135
|
+
selectedSessionExists,
|
|
136
|
+
selectedSessionKey,
|
|
137
|
+
selectedSessionPreferredModel,
|
|
138
|
+
setSelectedModel
|
|
139
|
+
]);
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
export function useSessionRunStatus(params: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
1
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
2
3
|
import type {
|
|
3
4
|
ChatRunView,
|
|
@@ -20,6 +21,7 @@ export type SendMessageParams = {
|
|
|
20
21
|
stopSupported?: boolean;
|
|
21
22
|
stopReason?: string;
|
|
22
23
|
restoreDraftOnError?: boolean;
|
|
24
|
+
composerNodes?: ChatComposerNode[];
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export type ActiveRunState = {
|
|
@@ -65,6 +67,7 @@ export type UseChatStreamControllerParams = {
|
|
|
65
67
|
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
66
68
|
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
67
69
|
setDraft: Dispatch<SetStateAction<string>>;
|
|
70
|
+
setComposerNodes: Dispatch<SetStateAction<ChatComposerNode[]>>;
|
|
68
71
|
refetchSessions: () => Promise<unknown>;
|
|
69
72
|
refetchHistory: () => Promise<unknown>;
|
|
70
73
|
};
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
2
|
import { ChatInputBar } from '@nextclaw/agent-chat-ui';
|
|
3
3
|
import {
|
|
4
4
|
buildChatSlashItems,
|
|
5
5
|
buildModelStateHint,
|
|
6
6
|
buildModelToolbarSelect,
|
|
7
|
-
buildSelectedSkillItems,
|
|
8
7
|
buildSkillPickerModel,
|
|
9
8
|
buildThinkingToolbarSelect,
|
|
10
|
-
resolveSlashQuery,
|
|
11
9
|
type ChatModelRecord,
|
|
12
10
|
type ChatSkillRecord,
|
|
13
11
|
type ChatThinkingLevel
|
|
14
12
|
} from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
15
|
-
import { useChatInputBarController } from '@/components/chat/chat-input/chat-input-bar.controller';
|
|
16
13
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
17
14
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
18
15
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
@@ -72,6 +69,7 @@ export function ChatInputBarContainer() {
|
|
|
72
69
|
const presenter = usePresenter();
|
|
73
70
|
const { language } = useI18n();
|
|
74
71
|
const snapshot = useChatInputStore((state) => state.snapshot);
|
|
72
|
+
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
|
75
73
|
|
|
76
74
|
const officialSkillBadgeLabel = useMemo(() => {
|
|
77
75
|
// Keep memo reactive to locale switches even though `t` is imported as a stable function.
|
|
@@ -110,31 +108,11 @@ export function ChatInputBarContainer() {
|
|
|
110
108
|
? t('chatInputPlaceholder')
|
|
111
109
|
: t('chatModelNoOptions');
|
|
112
110
|
|
|
113
|
-
const slashQuery = resolveSlashQuery(snapshot.draft);
|
|
114
111
|
const slashItems = useMemo(
|
|
115
112
|
() => buildChatSlashItems(skillRecords, slashQuery ?? '', slashTexts),
|
|
116
113
|
[slashQuery, skillRecords, slashTexts]
|
|
117
114
|
);
|
|
118
115
|
|
|
119
|
-
const controller = useChatInputBarController({
|
|
120
|
-
isSlashMode: slashQuery !== null,
|
|
121
|
-
slashItems,
|
|
122
|
-
isSlashLoading: snapshot.isSkillsLoading,
|
|
123
|
-
onSelectSlashItem: (item) => {
|
|
124
|
-
if (!item.value) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (!snapshot.selectedSkills.includes(item.value)) {
|
|
128
|
-
presenter.chatInputManager.selectSkills([...snapshot.selectedSkills, item.value]);
|
|
129
|
-
}
|
|
130
|
-
presenter.chatInputManager.setDraft('');
|
|
131
|
-
},
|
|
132
|
-
onSend: presenter.chatInputManager.send,
|
|
133
|
-
onStop: presenter.chatInputManager.stop,
|
|
134
|
-
isSending: snapshot.isSending,
|
|
135
|
-
canStopGeneration: snapshot.canStopGeneration
|
|
136
|
-
});
|
|
137
|
-
|
|
138
116
|
const selectedModelOption = modelRecords.find((option) => option.value === snapshot.selectedModel);
|
|
139
117
|
const selectedModelThinkingCapability = selectedModelOption?.thinkingCapability;
|
|
140
118
|
const thinkingSupportedLevels = selectedModelThinkingCapability?.supported ?? [];
|
|
@@ -183,27 +161,23 @@ export function ChatInputBarContainer() {
|
|
|
183
161
|
|
|
184
162
|
return (
|
|
185
163
|
<ChatInputBar
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
164
|
+
composer={{
|
|
165
|
+
nodes: snapshot.composerNodes,
|
|
166
|
+
placeholder: textareaPlaceholder,
|
|
167
|
+
disabled: inputDisabled,
|
|
168
|
+
onNodesChange: presenter.chatInputManager.setComposerNodes,
|
|
169
|
+
onSlashQueryChange: setSlashQuery
|
|
170
|
+
}}
|
|
191
171
|
slashMenu={{
|
|
192
|
-
isOpen: controller.isSlashPanelOpen,
|
|
193
172
|
isLoading: snapshot.isSkillsLoading,
|
|
194
173
|
items: slashItems,
|
|
195
|
-
activeIndex: controller.activeSlashIndex,
|
|
196
|
-
activeItem: controller.activeSlashItem,
|
|
197
174
|
texts: {
|
|
198
175
|
slashLoadingLabel: t('chatSlashLoading'),
|
|
199
176
|
slashSectionLabel: t('chatSlashSectionSkills'),
|
|
200
177
|
slashEmptyLabel: t('chatSlashNoResult'),
|
|
201
178
|
slashHintLabel: t('chatSlashHint'),
|
|
202
179
|
slashSkillHintLabel: t('chatSlashSkillHint')
|
|
203
|
-
}
|
|
204
|
-
onSelectItem: controller.onSelectSlashItem,
|
|
205
|
-
onOpenChange: controller.onSlashPanelOpenChange,
|
|
206
|
-
onSetActiveIndex: controller.onSetActiveSlashIndex
|
|
180
|
+
}
|
|
207
181
|
}}
|
|
208
182
|
hint={buildModelStateHint({
|
|
209
183
|
isModelOptionsLoading,
|
|
@@ -214,17 +188,14 @@ export function ChatInputBarContainer() {
|
|
|
214
188
|
configureProviderLabel: t('chatGoConfigureProvider')
|
|
215
189
|
}
|
|
216
190
|
})}
|
|
217
|
-
selectedItems={{
|
|
218
|
-
items: buildSelectedSkillItems(snapshot.selectedSkills, skillRecords),
|
|
219
|
-
onRemove: (key) => presenter.chatInputManager.selectSkills(snapshot.selectedSkills.filter((skill) => skill !== key))
|
|
220
|
-
}}
|
|
221
191
|
toolbar={{
|
|
222
192
|
selects: toolbarSelects,
|
|
223
193
|
accessories: [
|
|
224
194
|
{
|
|
225
195
|
key: 'attach',
|
|
226
|
-
label: t('
|
|
196
|
+
label: t('chatInputAttach'),
|
|
227
197
|
icon: 'paperclip',
|
|
198
|
+
iconOnly: true,
|
|
228
199
|
disabled: true,
|
|
229
200
|
tooltip: t('chatInputAttachComingSoon')
|
|
230
201
|
}
|
|
@@ -24,7 +24,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
24
24
|
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
25
25
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
26
26
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
27
|
-
const modelHydratedSessionKeyRef = useRef<string | null>(null);
|
|
28
27
|
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
29
28
|
const routeSessionKey = useMemo(
|
|
30
29
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
@@ -40,7 +39,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
40
39
|
sessions,
|
|
41
40
|
skillRecords,
|
|
42
41
|
selectedSession,
|
|
43
|
-
hydratedSessionModel,
|
|
44
42
|
historyMessages,
|
|
45
43
|
selectedSessionThinkingLevel,
|
|
46
44
|
sessionTypeOptions,
|
|
@@ -75,6 +73,7 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
75
73
|
selectedSessionKeyRef,
|
|
76
74
|
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
77
75
|
setDraft: presenter.chatInputManager.setDraft,
|
|
76
|
+
setComposerNodes: presenter.chatInputManager.setComposerNodes,
|
|
78
77
|
refetchSessions: sessionsQuery.refetch,
|
|
79
78
|
refetchHistory: historyQuery.refetch
|
|
80
79
|
},
|
|
@@ -139,13 +138,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
139
138
|
}, [presenter, sessionsQuery.refetch]);
|
|
140
139
|
|
|
141
140
|
useEffect(() => {
|
|
142
|
-
const shouldHydrateModelFromSession =
|
|
143
|
-
!isSending &&
|
|
144
|
-
!isAwaitingAssistantOutput &&
|
|
145
|
-
!sessionsQuery.isLoading &&
|
|
146
|
-
isProviderStateResolved &&
|
|
147
|
-
modelOptions.length > 0 &&
|
|
148
|
-
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
149
141
|
const shouldHydrateThinkingFromHistory =
|
|
150
142
|
!isSending &&
|
|
151
143
|
!isAwaitingAssistantOutput &&
|
|
@@ -164,7 +156,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
164
156
|
sendError: lastSendError,
|
|
165
157
|
isSending,
|
|
166
158
|
modelOptions,
|
|
167
|
-
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
168
159
|
sessionTypeOptions,
|
|
169
160
|
selectedSessionType,
|
|
170
161
|
...(shouldHydrateThinkingFromHistory ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
@@ -173,14 +164,10 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
173
164
|
skillRecords,
|
|
174
165
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
175
166
|
});
|
|
176
|
-
if (shouldHydrateModelFromSession) {
|
|
177
|
-
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
178
|
-
}
|
|
179
167
|
if (shouldHydrateThinkingFromHistory) {
|
|
180
168
|
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
181
169
|
}
|
|
182
170
|
if (!selectedSessionKey) {
|
|
183
|
-
modelHydratedSessionKeyRef.current = null;
|
|
184
171
|
thinkingHydratedSessionKeyRef.current = null;
|
|
185
172
|
}
|
|
186
173
|
presenter.chatSessionListManager.syncSnapshot({
|
|
@@ -220,7 +207,6 @@ export function LegacyChatPage({ view }: ChatPageProps) {
|
|
|
220
207
|
historyQuery.isLoading,
|
|
221
208
|
installedSkillsQuery.isLoading,
|
|
222
209
|
isAwaitingAssistantOutput,
|
|
223
|
-
hydratedSessionModel,
|
|
224
210
|
isProviderStateResolved,
|
|
225
211
|
isSending,
|
|
226
212
|
lastSendError,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
2
|
+
import {
|
|
3
|
+
createInitialChatComposerNodes,
|
|
4
|
+
createChatComposerNodesFromDraft,
|
|
5
|
+
deriveChatComposerDraft,
|
|
6
|
+
deriveSelectedSkillsFromComposer,
|
|
7
|
+
syncComposerSkills
|
|
8
|
+
} from '@/components/chat/chat-composer-state';
|
|
1
9
|
import { updateSession } from '@/api/config';
|
|
2
10
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
3
11
|
import { buildNewSessionKey } from '@/components/chat/chat-session-route';
|
|
@@ -36,6 +44,17 @@ export class ChatInputManager {
|
|
|
36
44
|
return next;
|
|
37
45
|
};
|
|
38
46
|
|
|
47
|
+
private isSameStringArray = (left: string[], right: string[]): boolean =>
|
|
48
|
+
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
49
|
+
|
|
50
|
+
private syncComposerSnapshot = (nodes: ChatComposerNode[]) => {
|
|
51
|
+
useChatInputStore.getState().setSnapshot({
|
|
52
|
+
composerNodes: nodes,
|
|
53
|
+
draft: deriveChatComposerDraft(nodes),
|
|
54
|
+
selectedSkills: deriveSelectedSkillsFromComposer(nodes)
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
39
58
|
syncSnapshot = (patch: Partial<ChatInputSnapshot>) => {
|
|
40
59
|
if (!this.hasSnapshotChanges(patch)) {
|
|
41
60
|
return;
|
|
@@ -46,8 +65,8 @@ export class ChatInputManager {
|
|
|
46
65
|
Object.prototype.hasOwnProperty.call(patch, 'selectedModel') ||
|
|
47
66
|
Object.prototype.hasOwnProperty.call(patch, 'selectedThinkingLevel')
|
|
48
67
|
) {
|
|
49
|
-
const
|
|
50
|
-
this.reconcileThinkingForModel(
|
|
68
|
+
const { selectedModel } = useChatInputStore.getState().snapshot;
|
|
69
|
+
this.reconcileThinkingForModel(selectedModel);
|
|
51
70
|
}
|
|
52
71
|
};
|
|
53
72
|
|
|
@@ -57,7 +76,16 @@ export class ChatInputManager {
|
|
|
57
76
|
if (value === prev) {
|
|
58
77
|
return;
|
|
59
78
|
}
|
|
60
|
-
|
|
79
|
+
this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
setComposerNodes = (next: SetStateAction<ChatComposerNode[]>) => {
|
|
83
|
+
const prev = useChatInputStore.getState().snapshot.composerNodes;
|
|
84
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
85
|
+
if (Object.is(value, prev)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.syncComposerSnapshot(value);
|
|
61
89
|
};
|
|
62
90
|
|
|
63
91
|
setPendingSessionType = (next: SetStateAction<string>) => {
|
|
@@ -76,14 +104,13 @@ export class ChatInputManager {
|
|
|
76
104
|
if (!message) {
|
|
77
105
|
return;
|
|
78
106
|
}
|
|
79
|
-
const requestedSkills = inputSnapshot
|
|
107
|
+
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
80
108
|
const hasSelectedSession = Boolean(sessionSnapshot.selectedSessionKey);
|
|
81
109
|
const sessionKey = sessionSnapshot.selectedSessionKey ?? buildNewSessionKey(sessionSnapshot.selectedAgentId);
|
|
82
110
|
if (!hasSelectedSession) {
|
|
83
111
|
this.uiManager.goToSession(sessionKey, { replace: true });
|
|
84
112
|
}
|
|
85
|
-
this.
|
|
86
|
-
this.setSelectedSkills([]);
|
|
113
|
+
this.setComposerNodes(createInitialChatComposerNodes());
|
|
87
114
|
await this.streamActionsManager.sendMessage({
|
|
88
115
|
message,
|
|
89
116
|
sessionKey,
|
|
@@ -94,7 +121,8 @@ export class ChatInputManager {
|
|
|
94
121
|
stopSupported: inputSnapshot.stopSupported,
|
|
95
122
|
stopReason: inputSnapshot.stopReason,
|
|
96
123
|
requestedSkills,
|
|
97
|
-
restoreDraftOnError: true
|
|
124
|
+
restoreDraftOnError: true,
|
|
125
|
+
composerNodes
|
|
98
126
|
});
|
|
99
127
|
};
|
|
100
128
|
|
|
@@ -132,12 +160,13 @@ export class ChatInputManager {
|
|
|
132
160
|
};
|
|
133
161
|
|
|
134
162
|
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
135
|
-
const
|
|
163
|
+
const snapshot = useChatInputStore.getState().snapshot;
|
|
164
|
+
const { selectedSkills: prev } = snapshot;
|
|
136
165
|
const value = this.resolveUpdateValue(prev, next);
|
|
137
|
-
if (
|
|
166
|
+
if (this.isSameStringArray(value, prev)) {
|
|
138
167
|
return;
|
|
139
168
|
}
|
|
140
|
-
|
|
169
|
+
this.syncComposerSnapshot(syncComposerSkills(snapshot.composerNodes, value, snapshot.skillRecords));
|
|
141
170
|
};
|
|
142
171
|
|
|
143
172
|
selectModel = (value: string) => {
|
|
@@ -174,15 +203,16 @@ export class ChatInputManager {
|
|
|
174
203
|
private reconcileThinkingForModel(model: string): void {
|
|
175
204
|
const snapshot = useChatInputStore.getState().snapshot;
|
|
176
205
|
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
177
|
-
const
|
|
178
|
-
|
|
206
|
+
const { selectedThinkingLevel } = snapshot;
|
|
207
|
+
const nextThinking = this.resolveThinkingForModel(modelOption, selectedThinkingLevel);
|
|
208
|
+
if (nextThinking !== selectedThinkingLevel) {
|
|
179
209
|
useChatInputStore.getState().setSnapshot({ selectedThinkingLevel: nextThinking });
|
|
180
210
|
}
|
|
181
211
|
}
|
|
182
212
|
|
|
183
213
|
private syncRemoteSessionType = async (normalizedType: string) => {
|
|
184
214
|
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
185
|
-
const selectedSessionKey = sessionSnapshot
|
|
215
|
+
const { selectedSessionKey } = sessionSnapshot;
|
|
186
216
|
if (!selectedSessionKey) {
|
|
187
217
|
return;
|
|
188
218
|
}
|
|
@@ -71,7 +71,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
71
71
|
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
72
72
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
73
73
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
74
|
-
const modelHydratedSessionKeyRef = useRef<string | null>(null);
|
|
75
74
|
const thinkingHydratedSessionKeyRef = useRef<string | null>(null);
|
|
76
75
|
const routeSessionKey = useMemo(
|
|
77
76
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
@@ -86,7 +85,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
86
85
|
sessions,
|
|
87
86
|
skillRecords,
|
|
88
87
|
selectedSession,
|
|
89
|
-
hydratedSessionModel,
|
|
90
88
|
selectedSessionThinkingLevel,
|
|
91
89
|
sessionTypeOptions,
|
|
92
90
|
defaultSessionType,
|
|
@@ -212,9 +210,17 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
212
210
|
await sessionsQuery.refetch();
|
|
213
211
|
} catch (error) {
|
|
214
212
|
if (payload.restoreDraftOnError) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
if (payload.composerNodes && payload.composerNodes.length > 0) {
|
|
214
|
+
presenter.chatInputManager.setComposerNodes((currentNodes) =>
|
|
215
|
+
currentNodes.length === 1 && currentNodes[0]?.type === 'text' && currentNodes[0].text.length === 0
|
|
216
|
+
? payload.composerNodes ?? currentNodes
|
|
217
|
+
: currentNodes
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
presenter.chatInputManager.setDraft((currentDraft) =>
|
|
221
|
+
currentDraft.trim().length === 0 ? payload.message : currentDraft
|
|
222
|
+
);
|
|
223
|
+
}
|
|
218
224
|
}
|
|
219
225
|
throw error;
|
|
220
226
|
}
|
|
@@ -270,13 +276,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
270
276
|
}, [presenter, sessionsQuery.refetch]);
|
|
271
277
|
|
|
272
278
|
useEffect(() => {
|
|
273
|
-
const shouldHydrateModelFromSession =
|
|
274
|
-
!isSending &&
|
|
275
|
-
!isAwaitingAssistantOutput &&
|
|
276
|
-
!sessionsQuery.isLoading &&
|
|
277
|
-
isProviderStateResolved &&
|
|
278
|
-
modelOptions.length > 0 &&
|
|
279
|
-
selectedSessionKey !== modelHydratedSessionKeyRef.current;
|
|
280
279
|
const shouldHydrateThinkingFromSession =
|
|
281
280
|
!isSending &&
|
|
282
281
|
!isAwaitingAssistantOutput &&
|
|
@@ -295,7 +294,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
295
294
|
sendError: lastSendError,
|
|
296
295
|
isSending,
|
|
297
296
|
modelOptions,
|
|
298
|
-
...(shouldHydrateModelFromSession ? { selectedModel: hydratedSessionModel } : {}),
|
|
299
297
|
sessionTypeOptions,
|
|
300
298
|
selectedSessionType,
|
|
301
299
|
...(shouldHydrateThinkingFromSession ? { selectedThinkingLevel: selectedSessionThinkingLevel } : {}),
|
|
@@ -304,14 +302,10 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
304
302
|
skillRecords,
|
|
305
303
|
isSkillsLoading: installedSkillsQuery.isLoading
|
|
306
304
|
});
|
|
307
|
-
if (shouldHydrateModelFromSession) {
|
|
308
|
-
modelHydratedSessionKeyRef.current = selectedSessionKey;
|
|
309
|
-
}
|
|
310
305
|
if (shouldHydrateThinkingFromSession) {
|
|
311
306
|
thinkingHydratedSessionKeyRef.current = selectedSessionKey;
|
|
312
307
|
}
|
|
313
308
|
if (!selectedSessionKey) {
|
|
314
|
-
modelHydratedSessionKeyRef.current = null;
|
|
315
309
|
thinkingHydratedSessionKeyRef.current = null;
|
|
316
310
|
}
|
|
317
311
|
presenter.chatSessionListManager.syncSnapshot({
|
|
@@ -349,7 +343,6 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
349
343
|
defaultSessionType,
|
|
350
344
|
installedSkillsQuery.isLoading,
|
|
351
345
|
isAwaitingAssistantOutput,
|
|
352
|
-
hydratedSessionModel,
|
|
353
346
|
isProviderStateResolved,
|
|
354
347
|
isSending,
|
|
355
348
|
lastSendError,
|