@nextclaw/ui 0.14.0 → 0.14.1-beta.0
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 +23 -0
- package/dist/assets/{api-B50yVdT8.js → api-C6EDHBDR.js} +1 -1
- package/dist/assets/channels-list-page-DKj6m1Hc.js +8 -0
- package/dist/assets/chat-page-BXngLJY6.js +105 -0
- package/dist/assets/config-split-page-CjlmJy8X.js +1 -0
- package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-B39X0pYp.js} +1 -1
- package/dist/assets/desktop-update-config-DfUQ8nly.js +1 -0
- package/dist/assets/{dist-NZRV-BxD.js → dist-BIYV2-Ia.js} +1 -1
- package/dist/assets/{dist-D19wiQwB.js → dist-CmMro3nV.js} +1 -1
- package/dist/assets/doc-browser-B6Wc_TIY.js +1 -0
- package/dist/assets/doc-browser-CDtUtcim.js +1 -0
- package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-DZbC9zVB.js} +1 -1
- package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-DDpgTzgx.js} +1 -1
- package/dist/assets/{external-link-528k6MDD.js → external-link-D2cBOCqn.js} +1 -1
- package/dist/assets/index-DTb_XVIH.js +103 -0
- package/dist/assets/index-ayiPYhf1.css +1 -0
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +1 -0
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +40 -0
- package/dist/assets/model-config-page-Bcb35AVv.js +1 -0
- package/dist/assets/plus-CYE1onU2.js +1 -0
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +1 -0
- package/dist/assets/providers-config-page-CQSibSwO.js +1 -0
- package/dist/assets/{react-Dsotw5vM.js → react-DYQXXGbH.js} +1 -1
- package/dist/assets/remote-BlJDbXOI.js +1 -0
- package/dist/assets/rotate-cw-BWbvFDAX.js +1 -0
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +1 -0
- package/dist/assets/{save-CHDnDTok.js → save-BNVTgBVn.js} +1 -1
- package/dist/assets/search-CKCzwLId.js +1 -0
- package/dist/assets/search-config-page-DG5Q2hF1.js +1 -0
- package/dist/assets/{secrets-config-page-95yaH1fa.js → secrets-config-page-BXn5Uuwp.js} +2 -2
- package/dist/assets/select-CIRwTNP4.js +41 -0
- package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-DKhVFtTE.js} +1 -1
- package/dist/assets/{use-config-TzwzTgg0.js → use-config-DgKM9VIU.js} +1 -1
- package/dist/assets/x-Cm3JjYut.js +1 -0
- package/dist/index.html +14 -27
- package/package.json +9 -9
- package/src/app/presenters/app.presenter.ts +2 -0
- package/src/features/account/components/account-panel.tsx +2 -2
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +39 -80
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +118 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +2 -17
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -24
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +0 -27
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +4 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
- package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
- package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
- package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
- package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
- package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
- package/src/features/chat/index.ts +1 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +28 -0
- package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
- package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
- package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
- package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
- package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
- package/src/features/chat/managers/chat-input.manager.ts +13 -13
- package/src/features/chat/managers/chat-run.manager.ts +3 -0
- package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
- package/src/features/chat/managers/chat-thread.manager.ts +4 -0
- package/src/features/chat/managers/chat-ui.manager.ts +2 -6
- package/src/features/chat/managers/recent-selection.manager.ts +21 -67
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
- package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
- package/src/features/chat/stores/chat-input.store.ts +7 -1
- package/src/features/chat/stores/recent-selection.store.ts +85 -0
- package/src/features/chat/types/chat-input-bar.types.ts +0 -1
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
- package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
- package/src/features/panel-apps/index.ts +2 -0
- package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
- package/src/shared/lib/api/chat-session-type.types.ts +7 -0
- package/src/shared/lib/api/ncp-session.types.ts +1 -0
- package/src/shared/lib/api/types.ts +3 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
- package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
- package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
- package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
- package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
- package/dist/assets/chat-page-CO27cl4v.js +0 -105
- package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
- package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
- package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
- package/dist/assets/doc-browser-BjHUczQb.js +0 -1
- package/dist/assets/es2015-BUIIKCNh.js +0 -41
- package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
- package/dist/assets/index-CMNrRhnG.js +0 -101
- package/dist/assets/index-UTmTW1eU.css +0 -1
- package/dist/assets/key-round-D3vD9dj7.js +0 -1
- package/dist/assets/loader-circle-DV9TSRns.js +0 -1
- package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
- package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
- package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
- package/dist/assets/notice-card-qqlbsOoR.js +0 -1
- package/dist/assets/plus-Bf9qhPDO.js +0 -1
- package/dist/assets/popover-BnmPOXe3.js +0 -1
- package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
- package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
- package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
- package/dist/assets/remote-DNMEy_c4.js +0 -1
- package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
- package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
- package/dist/assets/search-D7L-TCH-.js +0 -1
- package/dist/assets/search-config-page-CA3RVAef.js +0 -1
- package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
- package/dist/assets/tabs-CJvw5Lqp.js +0 -1
- package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
- package/dist/assets/trash-2-CSfX-BvU.js +0 -1
- package/dist/assets/x-fkIWvi07.js +0 -1
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
- package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
- /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NcpMessage } from '@nextclaw/ncp';
|
|
2
|
+
import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
|
|
2
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
|
|
4
5
|
import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
@@ -75,6 +76,39 @@ describe('ChatRunManager', () => {
|
|
|
75
76
|
expect(uiManager.goToSession).toHaveBeenCalledWith('materialized-session', { replace: true });
|
|
76
77
|
});
|
|
77
78
|
|
|
79
|
+
it('keeps the runtime-default sentinel in request metadata to override stale session model metadata', async () => {
|
|
80
|
+
const { manager } = createChatRunManager();
|
|
81
|
+
const sendEnvelope = vi.fn(async () => ({
|
|
82
|
+
sessionId: 'session-1',
|
|
83
|
+
userMessageId: 'user-message-1',
|
|
84
|
+
assistantMessageId: null,
|
|
85
|
+
runId: 'run-1',
|
|
86
|
+
}));
|
|
87
|
+
manager.setActiveRuntime({
|
|
88
|
+
sessionKey: 'session-1',
|
|
89
|
+
sendEnvelope,
|
|
90
|
+
abortCurrentRun: vi.fn(),
|
|
91
|
+
resumeCurrentSessionRun: vi.fn(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await manager.sendMessage({
|
|
95
|
+
message: 'hello',
|
|
96
|
+
sessionKey: 'session-1',
|
|
97
|
+
agentId: 'main',
|
|
98
|
+
sessionType: 'codex',
|
|
99
|
+
model: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(sendEnvelope).toHaveBeenCalledWith(
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
metadata: expect.objectContaining({
|
|
105
|
+
model: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
106
|
+
preferred_model: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
78
112
|
it('does not send through a runtime for another session', async () => {
|
|
79
113
|
const { manager } = createChatRunManager();
|
|
80
114
|
const sendEnvelope = vi.fn();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
|
|
2
3
|
import { updateNcpSession } from '@/shared/lib/api';
|
|
3
|
-
import { ChatSessionPreferenceSync } from '
|
|
4
|
+
import { ChatSessionPreferenceSync } from '@/features/chat/managers/chat-session-preference-sync.manager';
|
|
4
5
|
import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
5
6
|
import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
|
|
6
7
|
import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
|
|
@@ -25,6 +26,7 @@ describe('ChatSessionPreferenceSync', () => {
|
|
|
25
26
|
useChatInputStore.setState((state) => ({
|
|
26
27
|
snapshot: {
|
|
27
28
|
...state.snapshot,
|
|
29
|
+
modelOptions: [],
|
|
28
30
|
selectedModel: '',
|
|
29
31
|
selectedThinkingLevel: null
|
|
30
32
|
}
|
|
@@ -74,4 +76,144 @@ describe('ChatSessionPreferenceSync', () => {
|
|
|
74
76
|
});
|
|
75
77
|
});
|
|
76
78
|
});
|
|
79
|
+
|
|
80
|
+
it('clears the persisted model preference when runtime default is selected', async () => {
|
|
81
|
+
useChatInputStore.setState((state) => ({
|
|
82
|
+
snapshot: {
|
|
83
|
+
...state.snapshot,
|
|
84
|
+
selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
85
|
+
selectedThinkingLevel: null
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
useChatSessionListStore.setState((state) => ({
|
|
89
|
+
snapshot: {
|
|
90
|
+
...state.snapshot,
|
|
91
|
+
selectedSessionKey: 'session-1'
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
useChatThreadStore.setState((state) => ({
|
|
95
|
+
snapshot: {
|
|
96
|
+
...state.snapshot,
|
|
97
|
+
canDeleteSession: true
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const sync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
102
|
+
sync.syncSelectedSessionPreferences();
|
|
103
|
+
await vi.waitFor(() => {
|
|
104
|
+
expect(updateNcpSession).toHaveBeenCalledWith('session-1', {
|
|
105
|
+
preferredModel: null,
|
|
106
|
+
preferredThinking: null
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('uses the runtime-default thinking capability default when no session thinking is persisted', () => {
|
|
112
|
+
useChatInputStore.setState((state) => ({
|
|
113
|
+
snapshot: {
|
|
114
|
+
...state.snapshot,
|
|
115
|
+
modelOptions: [
|
|
116
|
+
{
|
|
117
|
+
value: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
118
|
+
modelLabel: 'Runtime default',
|
|
119
|
+
providerLabel: '',
|
|
120
|
+
isRuntimeDefault: true,
|
|
121
|
+
thinkingCapability: {
|
|
122
|
+
supported: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
123
|
+
default: 'high',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
selectedModel: '',
|
|
128
|
+
selectedThinkingLevel: null,
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const sync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
133
|
+
sync.syncInputSelection({
|
|
134
|
+
selectedSessionExists: true,
|
|
135
|
+
defaultModel: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
136
|
+
selectedSessionPreferredThinking: null,
|
|
137
|
+
fallbackPreferredThinking: null,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
141
|
+
selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
|
|
142
|
+
selectedThinkingLevel: 'high',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('does not rewrite the input snapshot when the resolved selection is unchanged', () => {
|
|
147
|
+
useChatInputStore.setState((state) => ({
|
|
148
|
+
snapshot: {
|
|
149
|
+
...state.snapshot,
|
|
150
|
+
modelOptions: [
|
|
151
|
+
{
|
|
152
|
+
value: 'openai/gpt-5',
|
|
153
|
+
modelLabel: 'GPT-5',
|
|
154
|
+
providerLabel: 'OpenAI',
|
|
155
|
+
thinkingCapability: null,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
selectedModel: 'openai/gpt-5',
|
|
159
|
+
selectedThinkingLevel: null,
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
const sync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
163
|
+
const listener = vi.fn();
|
|
164
|
+
const unsubscribe = useChatInputStore.subscribe(listener);
|
|
165
|
+
|
|
166
|
+
sync.syncInputSelection({
|
|
167
|
+
selectedSessionExists: true,
|
|
168
|
+
selectedSessionPreferredModel: 'openai/gpt-5',
|
|
169
|
+
selectedSessionPreferredThinking: null,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
unsubscribe();
|
|
173
|
+
expect(listener).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('applies a historical session preferred model after its summary arrives', () => {
|
|
177
|
+
useChatInputStore.setState((state) => ({
|
|
178
|
+
snapshot: {
|
|
179
|
+
...state.snapshot,
|
|
180
|
+
modelOptions: [
|
|
181
|
+
{
|
|
182
|
+
value: 'anthropic/claude-sonnet-4',
|
|
183
|
+
modelLabel: 'Claude Sonnet 4',
|
|
184
|
+
providerLabel: 'Anthropic',
|
|
185
|
+
thinkingCapability: null,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
value: 'openai/gpt-5',
|
|
189
|
+
modelLabel: 'GPT-5',
|
|
190
|
+
providerLabel: 'OpenAI',
|
|
191
|
+
thinkingCapability: null,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
selectedModel: 'anthropic/claude-sonnet-4',
|
|
195
|
+
selectedThinkingLevel: null,
|
|
196
|
+
}
|
|
197
|
+
}));
|
|
198
|
+
const sync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
199
|
+
|
|
200
|
+
sync.syncInputSelection({
|
|
201
|
+
selectedSessionKey: 'session-2',
|
|
202
|
+
selectedSessionExists: false,
|
|
203
|
+
fallbackPreferredModel: 'anthropic/claude-sonnet-4',
|
|
204
|
+
});
|
|
205
|
+
expect(useChatInputStore.getState().snapshot.selectedModel).toBe(
|
|
206
|
+
'anthropic/claude-sonnet-4',
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
sync.syncInputSelection({
|
|
210
|
+
selectedSessionKey: 'session-2',
|
|
211
|
+
selectedSessionExists: true,
|
|
212
|
+
selectedSessionPreferredModel: 'openai/gpt-5',
|
|
213
|
+
selectedSessionPreferredThinking: null,
|
|
214
|
+
fallbackPreferredModel: 'anthropic/claude-sonnet-4',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(useChatInputStore.getState().snapshot.selectedModel).toBe('openai/gpt-5');
|
|
218
|
+
});
|
|
77
219
|
});
|
|
@@ -483,6 +483,7 @@ describe('ChatThreadManager showContent', () => {
|
|
|
483
483
|
},
|
|
484
484
|
title: 'Example URL',
|
|
485
485
|
purpose: 'read',
|
|
486
|
+
placement: undefined,
|
|
486
487
|
});
|
|
487
488
|
await manager.handleUiShowContentEvent({
|
|
488
489
|
id: 'tool:call-show-content-1:show-content',
|
|
@@ -495,6 +496,7 @@ describe('ChatThreadManager showContent', () => {
|
|
|
495
496
|
},
|
|
496
497
|
title: 'Example URL',
|
|
497
498
|
purpose: 'read',
|
|
499
|
+
placement: undefined,
|
|
498
500
|
});
|
|
499
501
|
|
|
500
502
|
expect(uiManager.showContent).toHaveBeenCalledTimes(1);
|
|
@@ -541,6 +543,34 @@ describe('ChatThreadManager showContent', () => {
|
|
|
541
543
|
title: 'Reader',
|
|
542
544
|
});
|
|
543
545
|
});
|
|
546
|
+
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe('ChatThreadManager inline showContent', () => {
|
|
550
|
+
it('does not auto-open inline panel app content outside the message card', async () => {
|
|
551
|
+
const uiManager = createUiManager();
|
|
552
|
+
const manager = new ChatThreadManager(
|
|
553
|
+
uiManager,
|
|
554
|
+
{} as ConstructorParameters<typeof ChatThreadManager>[1],
|
|
555
|
+
{} as ConstructorParameters<typeof ChatThreadManager>[2],
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
await manager.handleUiShowContentEvent({
|
|
559
|
+
id: 'tool:call-show-content-inline:show-content',
|
|
560
|
+
toolCallId: 'call-show-content-inline',
|
|
561
|
+
target: {
|
|
562
|
+
type: 'panel_app',
|
|
563
|
+
payload: {
|
|
564
|
+
appId: 'reader',
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
title: 'Reader',
|
|
568
|
+
purpose: 'interact',
|
|
569
|
+
placement: 'inline',
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(uiManager.showContent).not.toHaveBeenCalled();
|
|
573
|
+
});
|
|
544
574
|
});
|
|
545
575
|
|
|
546
576
|
describe('ChatThreadManager visible workspace selection', () => {
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import { RecentSelectionManager } from '
|
|
1
|
+
import { RecentSelectionManager } from '@/features/chat/managers/recent-selection.manager';
|
|
2
|
+
import { useRecentSelectionStore } from '@/features/chat/stores/recent-selection.store';
|
|
2
3
|
|
|
3
4
|
describe('RecentSelectionManager', () => {
|
|
4
5
|
const storageKey = 'test.recent-selection-manager';
|
|
5
|
-
let storageState: Record<string, string>;
|
|
6
|
-
let storage: Pick<Storage, 'getItem' | 'setItem'>;
|
|
7
6
|
|
|
8
7
|
beforeEach(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
getItem: (key) => storageState[key] ?? null,
|
|
12
|
-
setItem: (key, value) => {
|
|
13
|
-
storageState[key] = value;
|
|
14
|
-
}
|
|
15
|
-
};
|
|
8
|
+
window.localStorage.clear();
|
|
9
|
+
useRecentSelectionStore.setState({ entriesByKey: {} });
|
|
16
10
|
});
|
|
17
11
|
|
|
18
12
|
it('stores recent values in LRU order and respects the size limit', () => {
|
|
19
|
-
const manager = new RecentSelectionManager({ storageKey, limit: 3
|
|
13
|
+
const manager = new RecentSelectionManager({ storageKey, limit: 3 });
|
|
20
14
|
|
|
21
15
|
manager.remember('openai/gpt-5');
|
|
22
16
|
manager.remember('anthropic/claude-sonnet-4');
|
|
@@ -31,7 +25,7 @@ describe('RecentSelectionManager', () => {
|
|
|
31
25
|
});
|
|
32
26
|
|
|
33
27
|
it('filters recent values by the currently available list and threshold', () => {
|
|
34
|
-
const manager = new RecentSelectionManager({ storageKey, limit: 4
|
|
28
|
+
const manager = new RecentSelectionManager({ storageKey, limit: 4 });
|
|
35
29
|
manager.remember('openai/gpt-5');
|
|
36
30
|
manager.remember('anthropic/claude-sonnet-4');
|
|
37
31
|
manager.remember('missing/model');
|
|
@@ -60,9 +54,38 @@ describe('RecentSelectionManager', () => {
|
|
|
60
54
|
});
|
|
61
55
|
|
|
62
56
|
it('returns an empty list when storage content is malformed', () => {
|
|
63
|
-
|
|
64
|
-
const manager = new RecentSelectionManager({ storageKey, limit: 3
|
|
57
|
+
window.localStorage.setItem(storageKey, '{broken-json');
|
|
58
|
+
const manager = new RecentSelectionManager({ storageKey, limit: 3 });
|
|
65
59
|
|
|
66
60
|
expect(manager.read()).toEqual([]);
|
|
67
61
|
});
|
|
62
|
+
|
|
63
|
+
it('does not mutate an empty store while reading recent values', () => {
|
|
64
|
+
const manager = new RecentSelectionManager({ storageKey, limit: 3 });
|
|
65
|
+
|
|
66
|
+
expect(manager.read()).toEqual([]);
|
|
67
|
+
expect(useRecentSelectionStore.getState().entriesByKey).toEqual({});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('keeps namespaced recent values separate from the global list', () => {
|
|
71
|
+
const manager = new RecentSelectionManager({ storageKey, limit: 3 });
|
|
72
|
+
|
|
73
|
+
manager.remember('openai/gpt-5');
|
|
74
|
+
manager.remember('anthropic/claude-sonnet-4', { namespace: 'codex' });
|
|
75
|
+
manager.remember('minimax/MiniMax-M2.7', { namespace: 'hermes' });
|
|
76
|
+
manager.remember('openai/gpt-5.1', { namespace: 'codex' });
|
|
77
|
+
|
|
78
|
+
expect(manager.read()).toEqual(['openai/gpt-5']);
|
|
79
|
+
expect(manager.read({ namespace: 'codex' })).toEqual([
|
|
80
|
+
'openai/gpt-5.1',
|
|
81
|
+
'anthropic/claude-sonnet-4'
|
|
82
|
+
]);
|
|
83
|
+
expect(
|
|
84
|
+
manager.resolveVisible({
|
|
85
|
+
namespace: 'hermes',
|
|
86
|
+
availableValues: ['minimax/MiniMax-M2.7', 'openai/gpt-5'],
|
|
87
|
+
minAvailableCount: 0
|
|
88
|
+
})
|
|
89
|
+
).toEqual(['minimax/MiniMax-M2.7']);
|
|
90
|
+
});
|
|
68
91
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type ChatDraftIntent = {
|
|
2
|
+
id: number;
|
|
3
|
+
prompt: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type ChatDraftIntentListener = (intent: ChatDraftIntent) => void;
|
|
7
|
+
|
|
8
|
+
export class ChatDraftIntentManager {
|
|
9
|
+
private nextId = 0;
|
|
10
|
+
private pendingIntent: ChatDraftIntent | null = null;
|
|
11
|
+
private readonly listeners = new Set<ChatDraftIntentListener>();
|
|
12
|
+
|
|
13
|
+
requestDraft = (prompt: string) => {
|
|
14
|
+
const normalizedPrompt = prompt.trim();
|
|
15
|
+
if (!normalizedPrompt) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const intent: ChatDraftIntent = {
|
|
19
|
+
id: this.nextId + 1,
|
|
20
|
+
prompt: normalizedPrompt,
|
|
21
|
+
};
|
|
22
|
+
this.nextId = intent.id;
|
|
23
|
+
this.pendingIntent = intent;
|
|
24
|
+
this.listeners.forEach((listener) => listener(intent));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
consumePending = (): ChatDraftIntent | null => {
|
|
28
|
+
const intent = this.pendingIntent;
|
|
29
|
+
this.pendingIntent = null;
|
|
30
|
+
return intent;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
markConsumed = (intentId: number) => {
|
|
34
|
+
if (this.pendingIntent?.id !== intentId) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.pendingIntent = null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
subscribe = (listener: ChatDraftIntentListener) => {
|
|
41
|
+
this.listeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
this.listeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -31,9 +31,6 @@ import { systemStatusManager } from '@/features/system-status';
|
|
|
31
31
|
import { shouldClearPendingProjectRootOverride } from '@/features/chat/features/session/utils/chat-run-metadata.utils';
|
|
32
32
|
|
|
33
33
|
function resolveModelForSend(value: string): string | undefined {
|
|
34
|
-
if (isRuntimeDefaultModelValue(value)) {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
34
|
return value || undefined;
|
|
38
35
|
}
|
|
39
36
|
|
|
@@ -55,16 +52,6 @@ export class ChatInputManager {
|
|
|
55
52
|
private sessionListManager: ChatSessionListManager
|
|
56
53
|
) {}
|
|
57
54
|
|
|
58
|
-
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
59
|
-
const current = useChatInputStore.getState().snapshot;
|
|
60
|
-
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
|
|
61
|
-
if (!Object.is(current[key], value)) {
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
55
|
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
69
56
|
if (typeof next === 'function') {
|
|
70
57
|
return (next as (value: T) => T)(prev);
|
|
@@ -75,6 +62,16 @@ export class ChatInputManager {
|
|
|
75
62
|
private isSameStringArray = (left: string[], right: string[]): boolean =>
|
|
76
63
|
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
77
64
|
|
|
65
|
+
private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
|
|
66
|
+
const current = useChatInputStore.getState().snapshot;
|
|
67
|
+
for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
|
|
68
|
+
if (!Object.is(current[key], value)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
|
|
78
75
|
private isRuntimeBlockedForSend = (): boolean =>
|
|
79
76
|
isNcpChatRuntimeBlocked(systemStatusManager.getStatusView());
|
|
80
77
|
|
|
@@ -351,6 +348,9 @@ export class ChatInputManager {
|
|
|
351
348
|
|
|
352
349
|
selectModel = (value: string) => {
|
|
353
350
|
this.setSelectedModel(value);
|
|
351
|
+
chatRecentModelsManager.remember(value, {
|
|
352
|
+
namespace: useChatInputStore.getState().snapshot.selectedSessionType,
|
|
353
|
+
});
|
|
354
354
|
if (!isRuntimeDefaultModelValue(value)) {
|
|
355
355
|
chatRecentModelsManager.remember(value);
|
|
356
356
|
}
|
|
@@ -61,6 +61,8 @@ export class ChatRunManager {
|
|
|
61
61
|
|
|
62
62
|
applyRunSnapshot = (snapshot: ChatRunSnapshot) => {
|
|
63
63
|
const isSending = snapshot.isSending || snapshot.isRunning;
|
|
64
|
+
const sessionKey =
|
|
65
|
+
snapshot.routeSessionKey ?? snapshot.materializedSessionKey ?? null;
|
|
64
66
|
useChatInputStore.getState().setSnapshot({
|
|
65
67
|
canStopGeneration: snapshot.isRunning,
|
|
66
68
|
stopDisabledReason: snapshot.isRunning ? null : '__preparing__',
|
|
@@ -70,6 +72,7 @@ export class ChatRunManager {
|
|
|
70
72
|
isSending,
|
|
71
73
|
});
|
|
72
74
|
useChatThreadStore.getState().setSnapshot({
|
|
75
|
+
sessionKey,
|
|
73
76
|
isHistoryLoading: snapshot.isHydrating,
|
|
74
77
|
messages: snapshot.visibleMessages,
|
|
75
78
|
isSending,
|
|
@@ -45,6 +45,7 @@ export class ChatSessionPreferenceSync {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
syncInputSelection = (params: {
|
|
48
|
+
selectedSessionKey?: string | null;
|
|
48
49
|
selectedSessionExists: boolean;
|
|
49
50
|
selectedSessionPreferredModel?: string;
|
|
50
51
|
fallbackPreferredModel?: string;
|
|
@@ -56,15 +57,14 @@ export class ChatSessionPreferenceSync {
|
|
|
56
57
|
defaultModel,
|
|
57
58
|
fallbackPreferredModel,
|
|
58
59
|
fallbackPreferredThinking,
|
|
60
|
+
selectedSessionKey = null,
|
|
59
61
|
selectedSessionExists,
|
|
60
62
|
selectedSessionPreferredModel,
|
|
61
63
|
selectedSessionPreferredThinking,
|
|
62
64
|
} = params;
|
|
63
65
|
const { snapshot } = useChatInputStore.getState();
|
|
64
66
|
const { modelOptions } = snapshot;
|
|
65
|
-
const selectedSessionKey = useChatThreadStore.getState().snapshot.sessionKey;
|
|
66
67
|
const sessionChanged = this.previousPreferenceSessionKey !== selectedSessionKey;
|
|
67
|
-
this.previousPreferenceSessionKey = selectedSessionKey;
|
|
68
68
|
const preserveCurrentPreference = sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists;
|
|
69
69
|
const selectedModel = resolveSelectedModelValue({
|
|
70
70
|
currentSelectedModel: snapshot.selectedModel,
|
|
@@ -88,10 +88,27 @@ export class ChatSessionPreferenceSync {
|
|
|
88
88
|
preferSessionPreferredThinking: sessionChanged,
|
|
89
89
|
preserveCurrentSelectedThinkingOnSessionChange: preserveCurrentPreference,
|
|
90
90
|
});
|
|
91
|
+
if (
|
|
92
|
+
snapshot.selectedModel === selectedModel &&
|
|
93
|
+
snapshot.selectedThinkingLevel === selectedThinking
|
|
94
|
+
) {
|
|
95
|
+
this.recordSyncedPreferenceSessionKey(selectedSessionKey, selectedSessionExists);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
91
98
|
useChatInputStore.getState().setSnapshot({
|
|
92
99
|
selectedModel,
|
|
93
100
|
selectedThinkingLevel: selectedThinking,
|
|
94
101
|
});
|
|
102
|
+
this.recordSyncedPreferenceSessionKey(selectedSessionKey, selectedSessionExists);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
private recordSyncedPreferenceSessionKey = (
|
|
106
|
+
selectedSessionKey: string | null,
|
|
107
|
+
selectedSessionExists: boolean,
|
|
108
|
+
): void => {
|
|
109
|
+
if (!selectedSessionKey || selectedSessionExists) {
|
|
110
|
+
this.previousPreferenceSessionKey = selectedSessionKey;
|
|
111
|
+
}
|
|
95
112
|
};
|
|
96
113
|
|
|
97
114
|
private enqueue = (next: QueuedSessionPreferenceSync): void => {
|
|
@@ -296,10 +296,14 @@ export class ChatThreadManager {
|
|
|
296
296
|
target: payload.target,
|
|
297
297
|
title: payload.title,
|
|
298
298
|
purpose: payload.purpose,
|
|
299
|
+
placement: payload.placement,
|
|
299
300
|
});
|
|
300
301
|
};
|
|
301
302
|
|
|
302
303
|
private showContent = async (request: ChatUiShowContentRequest): Promise<void> => {
|
|
304
|
+
if (request.placement === 'inline') {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
303
307
|
if (request.target.type === 'file') {
|
|
304
308
|
this.openFilePreview({
|
|
305
309
|
path: request.target.payload.path,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createPanelAppResourceUri,
|
|
7
7
|
createPanelAppRightPanelResourceTarget,
|
|
8
8
|
} from '@/features/right-panel-resources';
|
|
9
|
+
import { findPanelAppEntryByDisplayId } from '@/features/panel-apps';
|
|
9
10
|
import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
10
11
|
import { nextclawClient, type PanelAppEntryView } from '@/shared/lib/api';
|
|
11
12
|
|
|
@@ -26,11 +27,6 @@ type ChatUiDisplayContentTarget = Extract<ChatUiShowContentRequest['target'], {
|
|
|
26
27
|
|
|
27
28
|
const noopConfirm: ChatUiActions['confirm'] = async () => false;
|
|
28
29
|
|
|
29
|
-
function isPanelAppEntryMatch(entry: PanelAppEntryView, value: string): boolean {
|
|
30
|
-
const normalizedValue = value.trim();
|
|
31
|
-
return [entry.id, entry.appId, entry.fileName, entry.title].some((candidate) => candidate.trim() === normalizedValue);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
30
|
export class ChatUiManager {
|
|
35
31
|
private state: ChatUiState = {
|
|
36
32
|
pathname: ''
|
|
@@ -84,7 +80,7 @@ export class ChatUiManager {
|
|
|
84
80
|
return null;
|
|
85
81
|
}
|
|
86
82
|
try {
|
|
87
|
-
return (await nextclawClient.panelApps.listPanelApps()).entries
|
|
83
|
+
return findPanelAppEntryByDisplayId((await nextclawClient.panelApps.listPanelApps()).entries, normalizedValue);
|
|
88
84
|
} catch {
|
|
89
85
|
return null;
|
|
90
86
|
}
|
|
@@ -1,49 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRecentSelectionStore,
|
|
3
|
+
normalizeRecentSelectionList,
|
|
4
|
+
normalizeRecentSelectionValue,
|
|
5
|
+
resolveRecentSelectionKey,
|
|
6
|
+
type RecentSelectionScope,
|
|
7
|
+
} from '@/features/chat/stores/recent-selection.store';
|
|
8
|
+
|
|
1
9
|
type RecentSelectionManagerOptions = {
|
|
2
10
|
storageKey: string;
|
|
3
11
|
limit: number;
|
|
4
|
-
storage?: Pick<Storage, 'getItem' | 'setItem'> | null;
|
|
5
12
|
};
|
|
6
13
|
|
|
7
14
|
type VisibleRecentSelectionParams = {
|
|
8
15
|
availableValues: string[];
|
|
9
16
|
minAvailableCount: number;
|
|
10
17
|
limit?: number;
|
|
11
|
-
};
|
|
18
|
+
} & RecentSelectionScope;
|
|
12
19
|
|
|
13
20
|
export class RecentSelectionManager {
|
|
14
21
|
constructor(private readonly options: RecentSelectionManagerOptions) {}
|
|
15
22
|
|
|
16
|
-
read = (): string[] => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
return this.normalizeList(JSON.parse(storage.getItem(this.options.storageKey) ?? '[]'));
|
|
23
|
-
} catch {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
23
|
+
read = (scope: RecentSelectionScope = {}): string[] => {
|
|
24
|
+
const key = resolveRecentSelectionKey(this.options, scope);
|
|
25
|
+
return normalizeRecentSelectionList(useRecentSelectionStore.getState().entriesByKey[key], this.options.limit);
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
remember = (value: string): string[] => {
|
|
29
|
-
const normalizedValue =
|
|
28
|
+
remember = (value: string, scope: RecentSelectionScope = {}): string[] => {
|
|
29
|
+
const normalizedValue = normalizeRecentSelectionValue(value);
|
|
30
30
|
if (!normalizedValue) {
|
|
31
|
-
return this.read();
|
|
31
|
+
return this.read(scope);
|
|
32
32
|
}
|
|
33
|
-
const
|
|
34
|
-
this.
|
|
33
|
+
const key = resolveRecentSelectionKey(this.options, scope);
|
|
34
|
+
const next = [normalizedValue, ...this.read(scope).filter((item) => item !== normalizedValue)]
|
|
35
|
+
.slice(0, this.options.limit);
|
|
36
|
+
useRecentSelectionStore.getState().setEntries(key, next);
|
|
35
37
|
return next;
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
resolveVisible = (params: VisibleRecentSelectionParams): string[] => {
|
|
39
|
-
const availableValues =
|
|
41
|
+
const availableValues = normalizeRecentSelectionList(params.availableValues, Number.POSITIVE_INFINITY);
|
|
40
42
|
if (availableValues.length <= params.minAvailableCount) {
|
|
41
43
|
return [];
|
|
42
44
|
}
|
|
43
45
|
const availableSet = new Set(availableValues);
|
|
44
46
|
const visible: string[] = [];
|
|
45
47
|
const maxVisibleItems = Math.max(1, params.limit ?? this.options.limit);
|
|
46
|
-
for (const value of this.read()) {
|
|
48
|
+
for (const value of this.read(params)) {
|
|
47
49
|
if (!availableSet.has(value) || visible.includes(value)) {
|
|
48
50
|
continue;
|
|
49
51
|
}
|
|
@@ -54,52 +56,4 @@ export class RecentSelectionManager {
|
|
|
54
56
|
}
|
|
55
57
|
return visible;
|
|
56
58
|
};
|
|
57
|
-
|
|
58
|
-
private write = (values: string[]): void => {
|
|
59
|
-
const storage = this.getStorage();
|
|
60
|
-
if (!storage) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
storage.setItem(this.options.storageKey, JSON.stringify(this.normalizeList(values)));
|
|
65
|
-
} catch {
|
|
66
|
-
// Ignore storage write failures and keep the runtime behavior deterministic.
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
private getStorage = (): Storage | null => {
|
|
71
|
-
if (Object.prototype.hasOwnProperty.call(this.options, 'storage')) {
|
|
72
|
-
return (this.options.storage as Storage | null | undefined) ?? null;
|
|
73
|
-
}
|
|
74
|
-
if (typeof window === 'undefined') {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return window.localStorage;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
private normalizeList = (values: unknown, limit = this.options.limit): string[] => {
|
|
81
|
-
if (!Array.isArray(values)) {
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
const deduped: string[] = [];
|
|
85
|
-
for (const value of values) {
|
|
86
|
-
const normalized = this.normalizeValue(value);
|
|
87
|
-
if (!normalized || deduped.includes(normalized)) {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
deduped.push(normalized);
|
|
91
|
-
if (deduped.length >= limit) {
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return deduped;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
private normalizeValue = (value: unknown): string | null => {
|
|
99
|
-
if (typeof value !== 'string') {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const normalized = value.trim();
|
|
103
|
-
return normalized.length > 0 ? normalized : null;
|
|
104
|
-
};
|
|
105
59
|
}
|