@nextclaw/ui 0.17.2 → 0.18.1
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 +17 -0
- package/dist/assets/{api-CuPWhNDM.js → api-BJioGtWS.js} +1 -1
- package/dist/assets/{appearance-settings-page-DRjg6UP3.js → appearance-settings-page-CR2LJW8I.js} +1 -1
- package/dist/assets/{book-open-B4ovWX14.js → book-open-aDv6R1av.js} +1 -1
- package/dist/assets/{channels-list-page-B26GXq63.js → channels-list-page-BXyx_gxw.js} +1 -1
- package/dist/assets/{chat-page-QA0dpNyU.js → chat-page-OP5V5mAm.js} +1 -1
- package/dist/assets/{config-split-page-Box_-aBK.js → config-split-page-Bs1wArsX.js} +1 -1
- package/dist/assets/{confirm-dialog-uqcCxomz.js → confirm-dialog-BFatCZUq.js} +1 -1
- package/dist/assets/{desktop-update-config-DkDor159.js → desktop-update-config-eb7REelt.js} +1 -1
- package/dist/assets/{dist-CZLIZntJ.js → dist-CV9wIyME.js} +1 -1
- package/dist/assets/{dist-Co2G-YjN.js → dist-D1I3blO_.js} +1 -1
- package/dist/assets/{doc-browser-CbCBDdap.js → doc-browser-BFfstBoh.js} +1 -1
- package/dist/assets/doc-browser-DEdoMb3X.js +1 -0
- package/dist/assets/{doc-browser-context-aeZu0FHR.js → doc-browser-context-BcwcVwX_.js} +1 -1
- package/dist/assets/{form-actions-C3Pa1HQb.js → form-actions-YSnn7q_U.js} +1 -1
- package/dist/assets/index-DotCWGD1.js +109 -0
- package/dist/assets/loader-circle-CsNiT6VM.js +1 -0
- package/dist/assets/mcp-marketplace-page-DfUCF-L5.js +1 -0
- package/dist/assets/{mcp-marketplace-page-BmqbIQQQ.js → mcp-marketplace-page-ZTHpQTvi.js} +1 -1
- package/dist/assets/{model-config-page-BXUoZEEZ.js → model-config-page-BOhaenKN.js} +1 -1
- package/dist/assets/{navigation-link-DngetDmN.js → navigation-link-7oSjAC9R.js} +1 -1
- package/dist/assets/plus-D6UN8XN5.js +1 -0
- package/dist/assets/{provider-scoped-model-input-C4a2TuV7.js → provider-scoped-model-input-DlohNUbq.js} +1 -1
- package/dist/assets/{providers-config-page-CHSka1kQ.js → providers-config-page-eYaDPENc.js} +1 -1
- package/dist/assets/{react-CPH0zySO.js → react-7zG0fz1I.js} +1 -1
- package/dist/assets/{refresh-cw-vTYZqC0b.js → refresh-cw-BUg7hI3u.js} +1 -1
- package/dist/assets/remote-Cx_8UTi5.js +1 -0
- package/dist/assets/rotate-cw-B3qmXtfJ.js +1 -0
- package/dist/assets/{runtime-config-page-V75_zqXa.js → runtime-config-page-D5lV05VE.js} +1 -1
- package/dist/assets/{save-lWb6TI69.js → save-CTCYuZqu.js} +1 -1
- package/dist/assets/{search-BuUX_0KY.js → search-CfFUpaf7.js} +1 -1
- package/dist/assets/{search-config-page-rlkRJZCF.js → search-config-page-DAWe6n7E.js} +1 -1
- package/dist/assets/{secrets-config-page-BxCEc8r5.js → secrets-config-page-DyFeZDAi.js} +1 -1
- package/dist/assets/security-config-DoVoel5I.js +1 -0
- package/dist/assets/{select-oalsltZV.js → select-MSvkWmPP.js} +1 -1
- package/dist/assets/{settings-2-Bd9xiNAY.js → settings-2-DXUfAWgg.js} +1 -1
- package/dist/assets/skeleton-DDFivi9Y.js +1 -0
- package/dist/assets/{tag-chip-B324d_Xk.js → tag-chip-u8g0b0Nf.js} +1 -1
- package/dist/assets/x-DdOEUmR4.js +1 -0
- package/dist/index.html +19 -19
- package/package.json +6 -6
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +102 -1
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +19 -17
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +46 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +31 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-preference-persistence.test.tsx +110 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-slash-commands.test.tsx +20 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +138 -54
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +3 -2
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +30 -3
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-persistence.ts +88 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts +25 -2
- package/src/features/chat/features/session/hooks/__tests__/use-chat-session-update.test.tsx +27 -0
- package/src/features/chat/features/session/hooks/use-chat-session-update.ts +17 -6
- package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
- package/dist/assets/doc-browser-DUqckL2E.js +0 -1
- package/dist/assets/index-CCWgeWoR.js +0 -109
- package/dist/assets/loader-circle-DfbWUPdI.js +0 -1
- package/dist/assets/mcp-marketplace-page-B2D7ciJL.js +0 -1
- package/dist/assets/plus-wNpOxBfP.js +0 -1
- package/dist/assets/remote-DgfRmokf.js +0 -1
- package/dist/assets/rotate-cw-L_seGUwq.js +0 -1
- package/dist/assets/security-config-Cqxe13za.js +0 -1
- package/dist/assets/skeleton-D72EdBNa.js +0 -1
- package/dist/assets/x-Cps-je1K.js +0 -1
|
@@ -180,6 +180,52 @@ describe('useSessionConversationController backend run queue', () => {
|
|
|
180
180
|
expect(params.resetComposer).toHaveBeenCalledTimes(1);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
it('sends a preset message without clearing the existing composer draft', async () => {
|
|
184
|
+
const send = createSendMock();
|
|
185
|
+
const params = createControllerParams({ isRunning: false, send });
|
|
186
|
+
const paramsWithSelectedSkill = {
|
|
187
|
+
...params,
|
|
188
|
+
inputSnapshot: {
|
|
189
|
+
...params.inputSnapshot,
|
|
190
|
+
selectedSkills: ['skill-ref'],
|
|
191
|
+
skillRecords: [{ ref: 'skill-ref', name: 'Selected skill' }],
|
|
192
|
+
},
|
|
193
|
+
} as unknown as Parameters<typeof useSessionConversationController>[0];
|
|
194
|
+
const { result } = renderHook(() => useSessionConversationController(paramsWithSelectedSkill));
|
|
195
|
+
|
|
196
|
+
await act(async () => {
|
|
197
|
+
await result.current.sendPresetMessage('Update this session title');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const envelope = send.mock.calls[0]?.[0];
|
|
201
|
+
expect(envelope).toMatchObject({
|
|
202
|
+
sessionId: 'session-1',
|
|
203
|
+
message: expect.objectContaining({
|
|
204
|
+
parts: [{ type: 'text', text: 'Update this session title' }],
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
expect(envelope?.metadata).not.toHaveProperty('requested_skill_refs');
|
|
208
|
+
expect(params.resetComposer).not.toHaveBeenCalled();
|
|
209
|
+
expect(params.restoreComposer).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('keeps the existing composer draft untouched when a preset message fails', async () => {
|
|
213
|
+
const sendError = new Error('preset submission failed');
|
|
214
|
+
const send = vi.fn<TestAgentSend>(async () => {
|
|
215
|
+
throw sendError;
|
|
216
|
+
});
|
|
217
|
+
const params = createControllerParams({ isRunning: false, send });
|
|
218
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
219
|
+
|
|
220
|
+
await act(async () => {
|
|
221
|
+
await expect(result.current.sendPresetMessage('Update title')).rejects.toThrow(sendError);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(params.resetComposer).not.toHaveBeenCalled();
|
|
225
|
+
expect(params.restoreComposer).not.toHaveBeenCalled();
|
|
226
|
+
expect(params.setSendError).toHaveBeenLastCalledWith(sendError.message);
|
|
227
|
+
});
|
|
228
|
+
|
|
183
229
|
it('projects a submitting queue row before the backend acknowledges the message', async () => {
|
|
184
230
|
let resolveSend!: (handle: NcpRunHandle) => void;
|
|
185
231
|
const send = vi.fn<TestAgentSend>(() => new Promise((resolve) => {
|
|
@@ -21,6 +21,18 @@ const MODEL_OPTIONS: ChatModelOption[] = [
|
|
|
21
21
|
},
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
+
const THINKING_MODEL_OPTIONS: ChatModelOption[] = [
|
|
25
|
+
{
|
|
26
|
+
value: 'openai/gpt-5.6',
|
|
27
|
+
modelLabel: 'GPT-5.6',
|
|
28
|
+
providerLabel: 'OpenAI',
|
|
29
|
+
thinkingCapability: {
|
|
30
|
+
supported: ['off', 'low', 'medium', 'high'],
|
|
31
|
+
default: 'high',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
24
36
|
describe('useSessionConversationInputState session preferences', () => {
|
|
25
37
|
beforeEach(() => {
|
|
26
38
|
window.localStorage.clear();
|
|
@@ -36,6 +48,25 @@ describe('useSessionConversationInputState session preferences', () => {
|
|
|
36
48
|
expect(result.current.inputSnapshot.composerFocusRequestId).toBe(1);
|
|
37
49
|
});
|
|
38
50
|
|
|
51
|
+
it('does not let first preference hydration overwrite an explicit off selection', () => {
|
|
52
|
+
const { result } = renderHook(() => useSessionConversationInputState());
|
|
53
|
+
|
|
54
|
+
act(() => {
|
|
55
|
+
result.current.inputActions.setSelectedThinkingLevel('off');
|
|
56
|
+
});
|
|
57
|
+
act(() => {
|
|
58
|
+
result.current.inputActions.syncSessionPreferences({
|
|
59
|
+
fallbackPreferredThinking: 'high',
|
|
60
|
+
modelOptions: THINKING_MODEL_OPTIONS,
|
|
61
|
+
selectedSessionExists: false,
|
|
62
|
+
selectedSessionKey: null,
|
|
63
|
+
selectedSessionType: 'native',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.current.inputSnapshot.selectedThinkingLevel).toBe('off');
|
|
68
|
+
});
|
|
69
|
+
|
|
39
70
|
it('restores the switched session model after its metadata becomes available', () => {
|
|
40
71
|
const { result } = renderHook(() => useSessionConversationInputState());
|
|
41
72
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { useSessionConversationPreferencePersistence } from '@/features/chat/features/conversation/hooks/use-session-conversation-preference-persistence';
|
|
5
|
+
import type { SessionConversationInputActions } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
updateSession: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@/features/chat/features/session/hooks/use-chat-session-update', () => ({
|
|
12
|
+
useChatSessionUpdate: () => mocks.updateSession,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
function createInputActions() {
|
|
16
|
+
return {
|
|
17
|
+
update: vi.fn(),
|
|
18
|
+
} as unknown as SessionConversationInputActions;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('useSessionConversationPreferencePersistence', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
mocks.updateSession.mockResolvedValue(undefined);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('persists explicit off for a selected session without a success toast', async () => {
|
|
28
|
+
const inputActions = createInputActions();
|
|
29
|
+
const { result } = renderHook(() => useSessionConversationPreferencePersistence({
|
|
30
|
+
inputActions,
|
|
31
|
+
selectedSessionKey: 'session-1',
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
act(() => result.current(
|
|
35
|
+
{ preferredThinking: 'off' },
|
|
36
|
+
{ selectedThinkingLevel: 'off' },
|
|
37
|
+
{ selectedThinkingLevel: 'high' },
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
await waitFor(() => expect(mocks.updateSession).toHaveBeenCalledWith({
|
|
41
|
+
invalidateSessionSkills: false,
|
|
42
|
+
sessionKey: 'session-1',
|
|
43
|
+
patch: { preferredThinking: 'off' },
|
|
44
|
+
successMessage: null,
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rolls back the latest failed thinking mutation only when it is still visible', async () => {
|
|
49
|
+
mocks.updateSession.mockRejectedValueOnce(new Error('save failed'));
|
|
50
|
+
const inputActions = createInputActions();
|
|
51
|
+
const { result } = renderHook(() => useSessionConversationPreferencePersistence({
|
|
52
|
+
inputActions,
|
|
53
|
+
selectedSessionKey: 'session-1',
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
act(() => result.current(
|
|
57
|
+
{ preferredThinking: 'off' },
|
|
58
|
+
{ selectedThinkingLevel: 'off' },
|
|
59
|
+
{ selectedThinkingLevel: 'high' },
|
|
60
|
+
));
|
|
61
|
+
|
|
62
|
+
await waitFor(() => expect(inputActions.update).toHaveBeenCalledOnce());
|
|
63
|
+
const rollback = vi.mocked(inputActions.update).mock.calls[0]?.[0];
|
|
64
|
+
expect(typeof rollback).toBe('function');
|
|
65
|
+
expect((rollback as (current: never) => unknown)({
|
|
66
|
+
selectedModel: 'openai/gpt-5.6',
|
|
67
|
+
selectedThinkingLevel: 'off',
|
|
68
|
+
} as never)).toEqual({ selectedThinkingLevel: 'high' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rolls back a failed runtime-default model selection using its UI value', async () => {
|
|
72
|
+
mocks.updateSession.mockRejectedValueOnce(new Error('save failed'));
|
|
73
|
+
const inputActions = createInputActions();
|
|
74
|
+
const { result } = renderHook(() => useSessionConversationPreferencePersistence({
|
|
75
|
+
inputActions,
|
|
76
|
+
selectedSessionKey: 'session-1',
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
act(() => result.current(
|
|
80
|
+
{ preferredModel: null },
|
|
81
|
+
{ selectedModel: '__runtime_default__' },
|
|
82
|
+
{ selectedModel: 'openai/gpt-5.6' },
|
|
83
|
+
));
|
|
84
|
+
|
|
85
|
+
await waitFor(() => expect(inputActions.update).toHaveBeenCalledOnce());
|
|
86
|
+
const rollback = vi.mocked(inputActions.update).mock.calls[0]?.[0];
|
|
87
|
+
expect((rollback as (current: never) => unknown)({
|
|
88
|
+
selectedModel: '__runtime_default__',
|
|
89
|
+
selectedThinkingLevel: 'high',
|
|
90
|
+
} as never)).toEqual({ selectedModel: 'openai/gpt-5.6' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not write a draft preference to a remote session', async () => {
|
|
94
|
+
const inputActions = createInputActions();
|
|
95
|
+
const { result } = renderHook(() => useSessionConversationPreferencePersistence({
|
|
96
|
+
inputActions,
|
|
97
|
+
selectedSessionKey: null,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
act(() => result.current(
|
|
101
|
+
{ preferredThinking: 'off' },
|
|
102
|
+
{ selectedThinkingLevel: 'off' },
|
|
103
|
+
{ selectedThinkingLevel: 'high' },
|
|
104
|
+
));
|
|
105
|
+
await Promise.resolve();
|
|
106
|
+
|
|
107
|
+
expect(mocks.updateSession).not.toHaveBeenCalled();
|
|
108
|
+
expect(inputActions.update).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -7,6 +7,7 @@ import { useSessionConversationSlashCommands } from '@/features/chat/features/co
|
|
|
7
7
|
const mocks = vi.hoisted(() => ({
|
|
8
8
|
compactNcpSessionContext: vi.fn(),
|
|
9
9
|
openSideChatDraft: vi.fn(),
|
|
10
|
+
sendPresetMessage: vi.fn(),
|
|
10
11
|
}));
|
|
11
12
|
|
|
12
13
|
vi.mock('sonner', () => ({
|
|
@@ -35,12 +36,14 @@ describe('useSessionConversationSlashCommands', () => {
|
|
|
35
36
|
it('exposes the runtime compaction command for the selected session', async () => {
|
|
36
37
|
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
37
38
|
language: 'en',
|
|
39
|
+
onSendPresetMessage: mocks.sendPresetMessage,
|
|
38
40
|
selectedSessionKey: ' session-1 ',
|
|
39
41
|
}));
|
|
40
42
|
const command = result.current.find((entry) => entry.key === 'compact-context');
|
|
41
43
|
|
|
42
44
|
expect(result.current.map(({ key, icon }) => ({ key, icon }))).toEqual([
|
|
43
45
|
{ key: 'side-chat', icon: 'message-square-plus' },
|
|
46
|
+
{ key: 'update-session-title', icon: 'command' },
|
|
44
47
|
{ key: 'compact-context', icon: 'list-collapse' },
|
|
45
48
|
]);
|
|
46
49
|
act(() => command?.onSelect());
|
|
@@ -56,6 +59,7 @@ describe('useSessionConversationSlashCommands', () => {
|
|
|
56
59
|
}));
|
|
57
60
|
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
58
61
|
language: 'en',
|
|
62
|
+
onSendPresetMessage: mocks.sendPresetMessage,
|
|
59
63
|
selectedSessionKey: 'session-1',
|
|
60
64
|
}));
|
|
61
65
|
const command = result.current.find((entry) => entry.key === 'compact-context');
|
|
@@ -83,6 +87,7 @@ describe('useSessionConversationSlashCommands', () => {
|
|
|
83
87
|
const onContextCompactingChange = vi.fn();
|
|
84
88
|
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
85
89
|
language: 'en',
|
|
90
|
+
onSendPresetMessage: mocks.sendPresetMessage,
|
|
86
91
|
selectedSessionKey: 'session-1',
|
|
87
92
|
onContextCompactingChange,
|
|
88
93
|
}));
|
|
@@ -100,9 +105,24 @@ describe('useSessionConversationSlashCommands', () => {
|
|
|
100
105
|
));
|
|
101
106
|
});
|
|
102
107
|
|
|
108
|
+
it('sends the localized title update prompt through the conversation callback', () => {
|
|
109
|
+
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
110
|
+
language: 'en',
|
|
111
|
+
onSendPresetMessage: mocks.sendPresetMessage,
|
|
112
|
+
selectedSessionKey: 'session-1',
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
act(() => result.current.find((entry) => entry.key === 'update-session-title')?.onSelect());
|
|
116
|
+
|
|
117
|
+
expect(mocks.sendPresetMessage).toHaveBeenCalledWith(
|
|
118
|
+
expect.stringContaining('sessions_update'),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
103
122
|
it('does not expose session commands before a session exists', () => {
|
|
104
123
|
const { result } = renderHook(() => useSessionConversationSlashCommands({
|
|
105
124
|
language: 'en',
|
|
125
|
+
onSendPresetMessage: mocks.sendPresetMessage,
|
|
106
126
|
selectedSessionKey: null,
|
|
107
127
|
}));
|
|
108
128
|
|
package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { buildNcpRequestEnvelope } from '@nextclaw/ncp-react';
|
|
|
3
3
|
import type { UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
4
4
|
import type { NcpAgentSendEnvelope, NcpRunHandle } from '@nextclaw/ncp';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createChatComposerNodesFromDraft,
|
|
8
|
+
deriveNcpMessagePartsFromComposer,
|
|
9
|
+
} from '@/features/chat/features/input/utils/chat-composer-state.utils';
|
|
7
10
|
import { isNcpChatSendDisabled } from '@/features/chat/features/input/utils/ncp-chat-input-availability.utils';
|
|
8
11
|
import { buildChatRunMetadata } from '@/features/chat/features/session/utils/chat-run-metadata.utils';
|
|
9
12
|
import { createNcpSessionId } from '@/features/chat/features/session/utils/ncp-session-adapter.utils';
|
|
@@ -74,6 +77,7 @@ type UseSessionConversationControllerParams = {
|
|
|
74
77
|
|
|
75
78
|
type BuildSubmissionDraftParams = {
|
|
76
79
|
readonly agentIsSending: boolean;
|
|
80
|
+
readonly composerSnapshot?: ComposerDraftSnapshot;
|
|
77
81
|
readonly inputSnapshot: SessionConversationInputSnapshot;
|
|
78
82
|
readonly inputQuery: SessionConversationInputQuery;
|
|
79
83
|
readonly isRuntimeBlocked: boolean;
|
|
@@ -87,6 +91,8 @@ type SubmissionDraft = {
|
|
|
87
91
|
readonly metadata: Record<string, unknown>;
|
|
88
92
|
};
|
|
89
93
|
|
|
94
|
+
type SubmissionComposerBehavior = 'preserve' | 'reset-and-restore';
|
|
95
|
+
|
|
90
96
|
type SubmittingQueuedInput = {
|
|
91
97
|
readonly id: string;
|
|
92
98
|
readonly preview: string;
|
|
@@ -113,6 +119,7 @@ function buildInputAvailabilitySnapshot(inputQuery: SessionConversationInputQuer
|
|
|
113
119
|
function buildSubmissionDraft(params: BuildSubmissionDraftParams): SubmissionDraft | null {
|
|
114
120
|
const {
|
|
115
121
|
agentIsSending,
|
|
122
|
+
composerSnapshot: requestedComposerSnapshot,
|
|
116
123
|
inputQuery,
|
|
117
124
|
inputSnapshot,
|
|
118
125
|
isRuntimeBlocked,
|
|
@@ -120,9 +127,16 @@ function buildSubmissionDraft(params: BuildSubmissionDraftParams): SubmissionDra
|
|
|
120
127
|
selectedAgentId,
|
|
121
128
|
sessionKey,
|
|
122
129
|
} = params;
|
|
130
|
+
const composerSnapshot: ComposerDraftSnapshot = requestedComposerSnapshot ?? {
|
|
131
|
+
text: inputSnapshot.text,
|
|
132
|
+
nodes: [...inputSnapshot.nodes],
|
|
133
|
+
selectedSkills: [...inputSnapshot.selectedSkills],
|
|
134
|
+
skillRecords: [...inputSnapshot.skillRecords],
|
|
135
|
+
attachments: [...inputSnapshot.attachments],
|
|
136
|
+
};
|
|
123
137
|
const currentParts = deriveNcpMessagePartsFromComposer(
|
|
124
|
-
[...
|
|
125
|
-
|
|
138
|
+
[...composerSnapshot.nodes],
|
|
139
|
+
composerSnapshot.attachments,
|
|
126
140
|
);
|
|
127
141
|
if (
|
|
128
142
|
isNcpChatSendDisabled({
|
|
@@ -134,13 +148,6 @@ function buildSubmissionDraft(params: BuildSubmissionDraftParams): SubmissionDra
|
|
|
134
148
|
) {
|
|
135
149
|
return null;
|
|
136
150
|
}
|
|
137
|
-
const composerSnapshot: ComposerDraftSnapshot = {
|
|
138
|
-
text: inputSnapshot.text,
|
|
139
|
-
nodes: [...inputSnapshot.nodes],
|
|
140
|
-
selectedSkills: [...inputSnapshot.selectedSkills],
|
|
141
|
-
skillRecords: [...inputSnapshot.skillRecords],
|
|
142
|
-
attachments: [...inputSnapshot.attachments],
|
|
143
|
-
};
|
|
144
151
|
return {
|
|
145
152
|
composerSnapshot,
|
|
146
153
|
metadata: buildChatRunMetadata({
|
|
@@ -165,11 +172,11 @@ function buildSubmissionDraft(params: BuildSubmissionDraftParams): SubmissionDra
|
|
|
165
172
|
: sessionKey
|
|
166
173
|
? inputQuery.selectedSession?.projectRoot ?? null
|
|
167
174
|
: inputSnapshot.pendingProjectRoot,
|
|
168
|
-
requestedSkills: [...
|
|
175
|
+
requestedSkills: [...composerSnapshot.selectedSkills],
|
|
169
176
|
skillRecords: inputQuery.skillRecords.filter((record) =>
|
|
170
|
-
|
|
177
|
+
composerSnapshot.selectedSkills.includes(record.ref)
|
|
171
178
|
),
|
|
172
|
-
composerNodes: [...
|
|
179
|
+
composerNodes: [...composerSnapshot.nodes],
|
|
173
180
|
sessionMaterialization: materializationContext
|
|
174
181
|
? {
|
|
175
182
|
kind: materializationContext.kind,
|
|
@@ -288,6 +295,72 @@ function useQueuedInputActions(params: {
|
|
|
288
295
|
return { deleteQueuedInput, editQueuedInput };
|
|
289
296
|
}
|
|
290
297
|
|
|
298
|
+
function useSubmissionDraftRunner(params: {
|
|
299
|
+
readonly agent: SessionConversationAgent;
|
|
300
|
+
readonly clearSubmittingInput: (input: SubmittingQueuedInput) => void;
|
|
301
|
+
readonly materializationContext?: SessionConversationMaterializationContext | null;
|
|
302
|
+
readonly onSessionMaterialized?: (sessionKey: string) => void;
|
|
303
|
+
readonly resetComposer: () => void;
|
|
304
|
+
readonly restoreComposer: (snapshot: ComposerDraftSnapshot) => void;
|
|
305
|
+
readonly runQueue: SessionRunQueue;
|
|
306
|
+
readonly sessionKey: string | null;
|
|
307
|
+
readonly setSendError: (message: string | null) => void;
|
|
308
|
+
readonly stageSubmittingInput: (envelope: NcpAgentSendEnvelope) => SubmittingQueuedInput | null;
|
|
309
|
+
}) {
|
|
310
|
+
const {
|
|
311
|
+
agent,
|
|
312
|
+
clearSubmittingInput,
|
|
313
|
+
materializationContext,
|
|
314
|
+
onSessionMaterialized,
|
|
315
|
+
resetComposer,
|
|
316
|
+
restoreComposer,
|
|
317
|
+
runQueue,
|
|
318
|
+
sessionKey,
|
|
319
|
+
setSendError,
|
|
320
|
+
stageSubmittingInput,
|
|
321
|
+
} = params;
|
|
322
|
+
return useCallback(async (
|
|
323
|
+
draft: SubmissionDraft,
|
|
324
|
+
composerBehavior: SubmissionComposerBehavior,
|
|
325
|
+
) => {
|
|
326
|
+
const submissionSessionKey =
|
|
327
|
+
sessionKey ?? (materializationContext ? null : createNcpSessionId());
|
|
328
|
+
const envelope = buildSubmissionEnvelope(draft, submissionSessionKey);
|
|
329
|
+
if (!envelope) return;
|
|
330
|
+
const queuedSubmission = agent.isRunning ? stageSubmittingInput(envelope) : null;
|
|
331
|
+
if (composerBehavior === 'reset-and-restore') resetComposer();
|
|
332
|
+
setSendError(null);
|
|
333
|
+
try {
|
|
334
|
+
const handle = await agent.send(envelope);
|
|
335
|
+
if (queuedSubmission && handle?.runId === null) {
|
|
336
|
+
await runQueue.refreshQueuedInputs().catch(() => undefined);
|
|
337
|
+
}
|
|
338
|
+
if (queuedSubmission) clearSubmittingInput(queuedSubmission);
|
|
339
|
+
const materializedSessionKey =
|
|
340
|
+
handle?.sessionId?.trim() || agent.snapshot.activeRun?.sessionId?.trim() || null;
|
|
341
|
+
if (!sessionKey && materializedSessionKey) {
|
|
342
|
+
onSessionMaterialized?.(materializedSessionKey);
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (queuedSubmission) clearSubmittingInput(queuedSubmission);
|
|
346
|
+
if (composerBehavior === 'reset-and-restore') restoreComposer(draft.composerSnapshot);
|
|
347
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}, [
|
|
351
|
+
agent,
|
|
352
|
+
clearSubmittingInput,
|
|
353
|
+
materializationContext,
|
|
354
|
+
onSessionMaterialized,
|
|
355
|
+
resetComposer,
|
|
356
|
+
restoreComposer,
|
|
357
|
+
runQueue,
|
|
358
|
+
sessionKey,
|
|
359
|
+
setSendError,
|
|
360
|
+
stageSubmittingInput,
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
|
|
291
364
|
export function useSessionConversationController(params: UseSessionConversationControllerParams) {
|
|
292
365
|
const {
|
|
293
366
|
agent,
|
|
@@ -343,6 +416,19 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
343
416
|
setSendError,
|
|
344
417
|
});
|
|
345
418
|
|
|
419
|
+
const submitDraft = useSubmissionDraftRunner({
|
|
420
|
+
agent,
|
|
421
|
+
clearSubmittingInput,
|
|
422
|
+
materializationContext,
|
|
423
|
+
onSessionMaterialized,
|
|
424
|
+
resetComposer,
|
|
425
|
+
restoreComposer,
|
|
426
|
+
runQueue,
|
|
427
|
+
sessionKey,
|
|
428
|
+
setSendError,
|
|
429
|
+
stageSubmittingInput,
|
|
430
|
+
});
|
|
431
|
+
|
|
346
432
|
const send = useCallback(async () => {
|
|
347
433
|
if (primaryAction === 'continue') {
|
|
348
434
|
await continueRun();
|
|
@@ -357,58 +443,55 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
357
443
|
selectedAgentId,
|
|
358
444
|
sessionKey,
|
|
359
445
|
});
|
|
360
|
-
if (
|
|
361
|
-
|
|
446
|
+
if (draft) {
|
|
447
|
+
await submitDraft(draft, 'reset-and-restore');
|
|
362
448
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
449
|
+
}, [
|
|
450
|
+
agent.isSending,
|
|
451
|
+
continueRun,
|
|
452
|
+
inputQuery,
|
|
453
|
+
inputSnapshot,
|
|
454
|
+
isRuntimeBlocked,
|
|
455
|
+
materializationContext,
|
|
456
|
+
primaryAction,
|
|
457
|
+
selectedAgentId,
|
|
458
|
+
sessionKey,
|
|
459
|
+
submitDraft,
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
const sendPresetMessage = useCallback(async (message: string) => {
|
|
463
|
+
const text = message.trim();
|
|
464
|
+
if (!text) {
|
|
367
465
|
return;
|
|
368
466
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
} catch (error) {
|
|
388
|
-
if (queuedSubmission) {
|
|
389
|
-
clearSubmittingInput(queuedSubmission);
|
|
390
|
-
}
|
|
391
|
-
restoreComposer(draft.composerSnapshot);
|
|
392
|
-
setSendError(error instanceof Error ? error.message : String(error));
|
|
393
|
-
throw error;
|
|
467
|
+
const draft = buildSubmissionDraft({
|
|
468
|
+
agentIsSending: agent.isSending,
|
|
469
|
+
composerSnapshot: {
|
|
470
|
+
text,
|
|
471
|
+
nodes: createChatComposerNodesFromDraft(text),
|
|
472
|
+
selectedSkills: [],
|
|
473
|
+
skillRecords: [],
|
|
474
|
+
attachments: [],
|
|
475
|
+
},
|
|
476
|
+
inputSnapshot,
|
|
477
|
+
inputQuery,
|
|
478
|
+
isRuntimeBlocked,
|
|
479
|
+
materializationContext,
|
|
480
|
+
selectedAgentId,
|
|
481
|
+
sessionKey,
|
|
482
|
+
});
|
|
483
|
+
if (draft) {
|
|
484
|
+
await submitDraft(draft, 'preserve');
|
|
394
485
|
}
|
|
395
486
|
}, [
|
|
396
|
-
agent,
|
|
397
|
-
clearSubmittingInput,
|
|
398
|
-
continueRun,
|
|
487
|
+
agent.isSending,
|
|
399
488
|
inputQuery,
|
|
400
489
|
inputSnapshot,
|
|
401
490
|
isRuntimeBlocked,
|
|
402
491
|
materializationContext,
|
|
403
|
-
onSessionMaterialized,
|
|
404
|
-
resetComposer,
|
|
405
|
-
restoreComposer,
|
|
406
492
|
selectedAgentId,
|
|
407
493
|
sessionKey,
|
|
408
|
-
|
|
409
|
-
stageSubmittingInput,
|
|
410
|
-
primaryAction,
|
|
411
|
-
runQueue,
|
|
494
|
+
submitDraft,
|
|
412
495
|
]);
|
|
413
496
|
|
|
414
497
|
const stop = useCallback(async () => {
|
|
@@ -426,6 +509,7 @@ export function useSessionConversationController(params: UseSessionConversationC
|
|
|
426
509
|
isSending,
|
|
427
510
|
queuedInputs,
|
|
428
511
|
send,
|
|
512
|
+
sendPresetMessage,
|
|
429
513
|
sendDisabled,
|
|
430
514
|
primaryAction,
|
|
431
515
|
stop,
|
package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts
CHANGED
|
@@ -196,7 +196,7 @@ function usePersistedConversationInputSnapshot(
|
|
|
196
196
|
});
|
|
197
197
|
}, [draftKey, initialSnapshot]);
|
|
198
198
|
|
|
199
|
-
return { snapshot, update };
|
|
199
|
+
return { draftKey, snapshot, update };
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
export const useSessionConversationInputState = (
|
|
@@ -204,7 +204,7 @@ export const useSessionConversationInputState = (
|
|
|
204
204
|
sessionKey: string | null = null,
|
|
205
205
|
) => {
|
|
206
206
|
const pendingProjectRoot = useChatThreadStore((state) => state.snapshot.draftProjectRoot);
|
|
207
|
-
const { snapshot, update } = usePersistedConversationInputSnapshot(
|
|
207
|
+
const { draftKey, snapshot, update } = usePersistedConversationInputSnapshot(
|
|
208
208
|
initialPrompt,
|
|
209
209
|
sessionKey,
|
|
210
210
|
);
|
|
@@ -268,6 +268,7 @@ export const useSessionConversationInputState = (
|
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
const preferenceActions = useSessionConversationPreferenceActions({
|
|
271
|
+
preferenceOwnerKey: draftKey,
|
|
271
272
|
selectedModel: snapshot.selectedModel,
|
|
272
273
|
selectedThinkingLevel: snapshot.selectedThinkingLevel,
|
|
273
274
|
updatePreferences: update,
|
package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type SessionConversationPreferenceSyncParams = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
type SessionConversationPreferenceActionsParams = {
|
|
25
|
+
readonly preferenceOwnerKey: string;
|
|
25
26
|
readonly selectedModel: ConversationPreferenceValue;
|
|
26
27
|
readonly selectedThinkingLevel: ThinkingLevel | null;
|
|
27
28
|
readonly updatePreferences: (patch: {
|
|
@@ -31,14 +32,23 @@ type SessionConversationPreferenceActionsParams = {
|
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
export const useSessionConversationPreferenceActions = ({
|
|
35
|
+
preferenceOwnerKey,
|
|
34
36
|
selectedModel: currentSelectedModel,
|
|
35
37
|
selectedThinkingLevel: currentSelectedThinkingLevel,
|
|
36
38
|
updatePreferences,
|
|
37
39
|
}: SessionConversationPreferenceActionsParams) => {
|
|
38
40
|
const previousPreferenceContextRef = useRef<string | undefined>(undefined);
|
|
41
|
+
const localThinkingSelectionRef = useRef<{
|
|
42
|
+
readonly ownerKey: string;
|
|
43
|
+
readonly value: ThinkingLevel | null;
|
|
44
|
+
} | null>(null);
|
|
39
45
|
const setSelectedThinkingLevel = useCallback((selectedThinkingLevel: ThinkingLevel | null) => {
|
|
46
|
+
localThinkingSelectionRef.current = {
|
|
47
|
+
ownerKey: preferenceOwnerKey,
|
|
48
|
+
value: selectedThinkingLevel,
|
|
49
|
+
};
|
|
40
50
|
updatePreferences({ selectedThinkingLevel });
|
|
41
|
-
}, [updatePreferences]);
|
|
51
|
+
}, [preferenceOwnerKey, updatePreferences]);
|
|
42
52
|
const syncSessionPreferences = useCallback(({
|
|
43
53
|
defaultModel,
|
|
44
54
|
fallbackPreferredModel,
|
|
@@ -54,6 +64,17 @@ export const useSessionConversationPreferenceActions = ({
|
|
|
54
64
|
const preferenceContextKey = JSON.stringify([selectedSessionType, selectedSessionKey]);
|
|
55
65
|
const preferenceContextChanged = previousPreferenceContextRef.current !== preferenceContextKey;
|
|
56
66
|
const preserveCurrentPreference = preferenceContextChanged && Boolean(selectedSessionKey) && !selectedSessionExists;
|
|
67
|
+
const localThinkingSelection = localThinkingSelectionRef.current;
|
|
68
|
+
const localThinkingSelectionAcknowledged =
|
|
69
|
+
localThinkingSelection?.ownerKey === preferenceOwnerKey &&
|
|
70
|
+
selectedSessionExists &&
|
|
71
|
+
selectedSessionPreferredThinking === localThinkingSelection.value;
|
|
72
|
+
if (localThinkingSelectionAcknowledged) {
|
|
73
|
+
localThinkingSelectionRef.current = null;
|
|
74
|
+
}
|
|
75
|
+
const preserveLocalThinkingSelection =
|
|
76
|
+
localThinkingSelection?.ownerKey === preferenceOwnerKey &&
|
|
77
|
+
!localThinkingSelectionAcknowledged;
|
|
57
78
|
const selectedModel = resolveSelectedModelValue({
|
|
58
79
|
currentSelectedModel: currentSelectedModel ?? undefined,
|
|
59
80
|
modelOptions,
|
|
@@ -73,7 +94,8 @@ export const useSessionConversationPreferenceActions = ({
|
|
|
73
94
|
defaultThinkingLevel:
|
|
74
95
|
(modelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
|
|
75
96
|
preferSessionPreferredThinking: preferenceContextChanged,
|
|
76
|
-
preserveCurrentSelectedThinkingOnSessionChange:
|
|
97
|
+
preserveCurrentSelectedThinkingOnSessionChange:
|
|
98
|
+
preserveCurrentPreference || preserveLocalThinkingSelection,
|
|
77
99
|
});
|
|
78
100
|
if (
|
|
79
101
|
currentSelectedModel !== selectedModel ||
|
|
@@ -84,7 +106,12 @@ export const useSessionConversationPreferenceActions = ({
|
|
|
84
106
|
if (!selectedSessionKey || selectedSessionExists) {
|
|
85
107
|
previousPreferenceContextRef.current = preferenceContextKey;
|
|
86
108
|
}
|
|
87
|
-
}, [
|
|
109
|
+
}, [
|
|
110
|
+
currentSelectedModel,
|
|
111
|
+
currentSelectedThinkingLevel,
|
|
112
|
+
preferenceOwnerKey,
|
|
113
|
+
updatePreferences,
|
|
114
|
+
]);
|
|
88
115
|
|
|
89
116
|
return useMemo(() => ({
|
|
90
117
|
setSelectedThinkingLevel,
|