@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/assets/{api-CuPWhNDM.js → api-BJioGtWS.js} +1 -1
  3. package/dist/assets/{appearance-settings-page-DRjg6UP3.js → appearance-settings-page-CR2LJW8I.js} +1 -1
  4. package/dist/assets/{book-open-B4ovWX14.js → book-open-aDv6R1av.js} +1 -1
  5. package/dist/assets/{channels-list-page-B26GXq63.js → channels-list-page-BXyx_gxw.js} +1 -1
  6. package/dist/assets/{chat-page-QA0dpNyU.js → chat-page-OP5V5mAm.js} +1 -1
  7. package/dist/assets/{config-split-page-Box_-aBK.js → config-split-page-Bs1wArsX.js} +1 -1
  8. package/dist/assets/{confirm-dialog-uqcCxomz.js → confirm-dialog-BFatCZUq.js} +1 -1
  9. package/dist/assets/{desktop-update-config-DkDor159.js → desktop-update-config-eb7REelt.js} +1 -1
  10. package/dist/assets/{dist-CZLIZntJ.js → dist-CV9wIyME.js} +1 -1
  11. package/dist/assets/{dist-Co2G-YjN.js → dist-D1I3blO_.js} +1 -1
  12. package/dist/assets/{doc-browser-CbCBDdap.js → doc-browser-BFfstBoh.js} +1 -1
  13. package/dist/assets/doc-browser-DEdoMb3X.js +1 -0
  14. package/dist/assets/{doc-browser-context-aeZu0FHR.js → doc-browser-context-BcwcVwX_.js} +1 -1
  15. package/dist/assets/{form-actions-C3Pa1HQb.js → form-actions-YSnn7q_U.js} +1 -1
  16. package/dist/assets/index-DotCWGD1.js +109 -0
  17. package/dist/assets/loader-circle-CsNiT6VM.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-DfUCF-L5.js +1 -0
  19. package/dist/assets/{mcp-marketplace-page-BmqbIQQQ.js → mcp-marketplace-page-ZTHpQTvi.js} +1 -1
  20. package/dist/assets/{model-config-page-BXUoZEEZ.js → model-config-page-BOhaenKN.js} +1 -1
  21. package/dist/assets/{navigation-link-DngetDmN.js → navigation-link-7oSjAC9R.js} +1 -1
  22. package/dist/assets/plus-D6UN8XN5.js +1 -0
  23. package/dist/assets/{provider-scoped-model-input-C4a2TuV7.js → provider-scoped-model-input-DlohNUbq.js} +1 -1
  24. package/dist/assets/{providers-config-page-CHSka1kQ.js → providers-config-page-eYaDPENc.js} +1 -1
  25. package/dist/assets/{react-CPH0zySO.js → react-7zG0fz1I.js} +1 -1
  26. package/dist/assets/{refresh-cw-vTYZqC0b.js → refresh-cw-BUg7hI3u.js} +1 -1
  27. package/dist/assets/remote-Cx_8UTi5.js +1 -0
  28. package/dist/assets/rotate-cw-B3qmXtfJ.js +1 -0
  29. package/dist/assets/{runtime-config-page-V75_zqXa.js → runtime-config-page-D5lV05VE.js} +1 -1
  30. package/dist/assets/{save-lWb6TI69.js → save-CTCYuZqu.js} +1 -1
  31. package/dist/assets/{search-BuUX_0KY.js → search-CfFUpaf7.js} +1 -1
  32. package/dist/assets/{search-config-page-rlkRJZCF.js → search-config-page-DAWe6n7E.js} +1 -1
  33. package/dist/assets/{secrets-config-page-BxCEc8r5.js → secrets-config-page-DyFeZDAi.js} +1 -1
  34. package/dist/assets/security-config-DoVoel5I.js +1 -0
  35. package/dist/assets/{select-oalsltZV.js → select-MSvkWmPP.js} +1 -1
  36. package/dist/assets/{settings-2-Bd9xiNAY.js → settings-2-DXUfAWgg.js} +1 -1
  37. package/dist/assets/skeleton-DDFivi9Y.js +1 -0
  38. package/dist/assets/{tag-chip-B324d_Xk.js → tag-chip-u8g0b0Nf.js} +1 -1
  39. package/dist/assets/x-DdOEUmR4.js +1 -0
  40. package/dist/index.html +19 -19
  41. package/package.json +6 -6
  42. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +102 -1
  43. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
  44. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +19 -17
  45. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +46 -0
  46. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +31 -0
  47. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-preference-persistence.test.tsx +110 -0
  48. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-slash-commands.test.tsx +20 -0
  49. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +138 -54
  50. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +3 -2
  51. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +30 -3
  52. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-persistence.ts +88 -0
  53. package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts +25 -2
  54. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-update.test.tsx +27 -0
  55. package/src/features/chat/features/session/hooks/use-chat-session-update.ts +17 -6
  56. package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
  57. package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
  58. package/dist/assets/doc-browser-DUqckL2E.js +0 -1
  59. package/dist/assets/index-CCWgeWoR.js +0 -109
  60. package/dist/assets/loader-circle-DfbWUPdI.js +0 -1
  61. package/dist/assets/mcp-marketplace-page-B2D7ciJL.js +0 -1
  62. package/dist/assets/plus-wNpOxBfP.js +0 -1
  63. package/dist/assets/remote-DgfRmokf.js +0 -1
  64. package/dist/assets/rotate-cw-L_seGUwq.js +0 -1
  65. package/dist/assets/security-config-Cqxe13za.js +0 -1
  66. package/dist/assets/skeleton-D72EdBNa.js +0 -1
  67. package/dist/assets/x-Cps-je1K.js +0 -1
@@ -0,0 +1,88 @@
1
+ import { useCallback, useRef } from 'react';
2
+
3
+ import type { SessionPatchUpdate, ThinkingLevel } from '@/shared/lib/api';
4
+ import { useChatSessionUpdate } from '@/features/chat/features/session/hooks/use-chat-session-update';
5
+ import type {
6
+ SessionConversationInputActions,
7
+ SessionConversationInputSnapshot,
8
+ } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
9
+
10
+ type SessionPreferencePatch = Pick<
11
+ SessionPatchUpdate,
12
+ 'preferredModel' | 'preferredThinking'
13
+ >;
14
+
15
+ type SessionPreferenceRollback = Pick<
16
+ SessionConversationInputSnapshot,
17
+ 'selectedModel' | 'selectedThinkingLevel'
18
+ >;
19
+
20
+ function ownsField<T extends object>(value: T, field: keyof T): boolean {
21
+ return Object.prototype.hasOwnProperty.call(value, field);
22
+ }
23
+
24
+ export function useSessionConversationPreferencePersistence(params: {
25
+ readonly inputActions: SessionConversationInputActions;
26
+ readonly selectedSessionKey: string | null;
27
+ }) {
28
+ const { inputActions, selectedSessionKey } = params;
29
+ const updateSession = useChatSessionUpdate();
30
+ const mutationSequenceRef = useRef(0);
31
+ const latestMutationByFieldRef = useRef(new Map<string, number>());
32
+ const persistenceQueueRef = useRef<Promise<void>>(Promise.resolve());
33
+
34
+ return useCallback((
35
+ patch: SessionPreferencePatch,
36
+ optimistic: Partial<SessionPreferenceRollback>,
37
+ rollback: Partial<SessionPreferenceRollback>,
38
+ ) => {
39
+ const sessionKey = selectedSessionKey?.trim();
40
+ if (!sessionKey) {
41
+ return;
42
+ }
43
+
44
+ const mutationId = ++mutationSequenceRef.current;
45
+ const modelMutationKey = `${sessionKey}:model`;
46
+ const thinkingMutationKey = `${sessionKey}:thinking`;
47
+ if (ownsField(patch, 'preferredModel')) {
48
+ latestMutationByFieldRef.current.set(modelMutationKey, mutationId);
49
+ }
50
+ if (ownsField(patch, 'preferredThinking')) {
51
+ latestMutationByFieldRef.current.set(thinkingMutationKey, mutationId);
52
+ }
53
+
54
+ const operation = persistenceQueueRef.current.then(() => updateSession({
55
+ invalidateSessionSkills: false,
56
+ sessionKey,
57
+ patch,
58
+ successMessage: null,
59
+ }));
60
+ persistenceQueueRef.current = operation.catch(() => undefined);
61
+
62
+ void operation.catch(() => {
63
+ inputActions.update((current) => {
64
+ const next: {
65
+ selectedModel?: string | null | undefined;
66
+ selectedThinkingLevel?: ThinkingLevel | null;
67
+ } = {};
68
+ if (
69
+ ownsField(patch, 'preferredModel') &&
70
+ latestMutationByFieldRef.current.get(modelMutationKey) === mutationId &&
71
+ current.selectedModel === optimistic.selectedModel &&
72
+ ownsField(rollback, 'selectedModel')
73
+ ) {
74
+ next.selectedModel = rollback.selectedModel;
75
+ }
76
+ if (
77
+ ownsField(patch, 'preferredThinking') &&
78
+ latestMutationByFieldRef.current.get(thinkingMutationKey) === mutationId &&
79
+ current.selectedThinkingLevel === optimistic.selectedThinkingLevel &&
80
+ ownsField(rollback, 'selectedThinkingLevel')
81
+ ) {
82
+ next.selectedThinkingLevel = rollback.selectedThinkingLevel;
83
+ }
84
+ return next;
85
+ });
86
+ });
87
+ }, [inputActions, selectedSessionKey, updateSession]);
88
+ }
@@ -8,9 +8,15 @@ import type { ChatSlashCommandDescriptor } from '@/features/chat/features/input/
8
8
  export function useSessionConversationSlashCommands(params: {
9
9
  language: I18nLanguage;
10
10
  onContextCompactingChange?: (sessionId: string, isCompacting: boolean) => void;
11
+ onSendPresetMessage: (message: string) => Promise<void> | void;
11
12
  selectedSessionKey?: string | null;
12
13
  }): readonly ChatSlashCommandDescriptor[] {
13
- const { language, onContextCompactingChange, selectedSessionKey } = params;
14
+ const {
15
+ language,
16
+ onContextCompactingChange,
17
+ onSendPresetMessage,
18
+ selectedSessionKey,
19
+ } = params;
14
20
  const presenter = usePresenter();
15
21
  const compactingSessionIdsRef = useRef(new Set<string>());
16
22
  const compactContext = useCallback(async (sessionId: string) => {
@@ -46,6 +52,17 @@ export function useSessionConversationSlashCommands(params: {
46
52
  keywords: ['side', 'chat', 'child', 'branch', 'new'],
47
53
  onSelect: () => presenter.chatThreadManager.openSideChatDraft(sessionId),
48
54
  },
55
+ {
56
+ key: 'update-session-title',
57
+ icon: 'command',
58
+ title: t('chatSlashCommandUpdateSessionTitleTitle', language),
59
+ description: t('chatSlashCommandUpdateSessionTitleDescription', language),
60
+ detailLines: [t('chatSlashCommandUpdateSessionTitleDetail', language)],
61
+ keywords: ['update', 'rename', 'title', 'session', '更新', '重命名', '标题', '会话'],
62
+ onSelect: () => void onSendPresetMessage(
63
+ t('chatSlashCommandUpdateSessionTitlePrompt', language),
64
+ ),
65
+ },
49
66
  {
50
67
  key: 'compact-context',
51
68
  icon: 'list-collapse',
@@ -56,5 +73,11 @@ export function useSessionConversationSlashCommands(params: {
56
73
  onSelect: () => void compactContext(sessionId),
57
74
  },
58
75
  ];
59
- }, [compactContext, language, presenter.chatThreadManager, selectedSessionKey]);
76
+ }, [
77
+ compactContext,
78
+ language,
79
+ onSendPresetMessage,
80
+ presenter.chatThreadManager,
81
+ selectedSessionKey,
82
+ ]);
60
83
  }
@@ -71,4 +71,31 @@ describe('useChatSessionUpdate', () => {
71
71
  });
72
72
  expect(toast.success).toHaveBeenCalledWith('Project directory updated');
73
73
  });
74
+
75
+ it('can update an inline preference without showing a success toast', async () => {
76
+ const queryClient = new QueryClient();
77
+ const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
78
+ mocks.updateNcpSession.mockResolvedValue({
79
+ sessionId: 'session-1',
80
+ updatedAt: '2026-08-15T00:00:00.000Z',
81
+ status: 'idle',
82
+ metadata: { preferred_thinking: 'off' },
83
+ });
84
+ const { result } = renderHook(() => useChatSessionUpdate(), {
85
+ wrapper: createWrapper(queryClient),
86
+ });
87
+
88
+ await act(async () => {
89
+ await result.current({
90
+ invalidateSessionSkills: false,
91
+ sessionKey: 'session-1',
92
+ patch: { preferredThinking: 'off' },
93
+ successMessage: null,
94
+ });
95
+ });
96
+
97
+ expect(toast.success).not.toHaveBeenCalled();
98
+ expect(mocks.upsertNcpSessionSummaryInQueryClient).toHaveBeenCalledOnce();
99
+ expect(invalidateQueriesSpy).not.toHaveBeenCalled();
100
+ });
74
101
  });
@@ -1,3 +1,4 @@
1
+ import { useCallback } from 'react';
1
2
  import { useQueryClient } from '@tanstack/react-query';
2
3
  import { toast } from 'sonner';
3
4
  import type { SessionPatchUpdate } from '@/shared/lib/api';
@@ -6,9 +7,10 @@ import { upsertNcpSessionSummaryInQueryClient } from '@/shared/lib/api';
6
7
  import { t } from '@/shared/lib/i18n';
7
8
 
8
9
  type UpdateChatSessionParams = {
10
+ invalidateSessionSkills?: boolean;
9
11
  sessionKey: string;
10
12
  patch: SessionPatchUpdate;
11
- successMessage?: string;
13
+ successMessage?: string | null;
12
14
  };
13
15
 
14
16
  type UpdateChatSessionLabelParams = {
@@ -19,18 +21,27 @@ type UpdateChatSessionLabelParams = {
19
21
  export function useChatSessionUpdate() {
20
22
  const queryClient = useQueryClient();
21
23
 
22
- return async (params: UpdateChatSessionParams): Promise<void> => {
23
- const { sessionKey, patch, successMessage } = params;
24
+ return useCallback(async (params: UpdateChatSessionParams): Promise<void> => {
25
+ const {
26
+ invalidateSessionSkills = true,
27
+ sessionKey,
28
+ patch,
29
+ successMessage,
30
+ } = params;
24
31
  try {
25
32
  const updated = await updateNcpSession(sessionKey, patch);
26
33
  upsertNcpSessionSummaryInQueryClient(queryClient, updated);
27
- await queryClient.invalidateQueries({ queryKey: ['ncp-session-skills', sessionKey] });
28
- toast.success(successMessage ?? t('configSavedApplied'));
34
+ if (invalidateSessionSkills) {
35
+ await queryClient.invalidateQueries({ queryKey: ['ncp-session-skills', sessionKey] });
36
+ }
37
+ if (successMessage !== null) {
38
+ toast.success(successMessage ?? t('configSavedApplied'));
39
+ }
29
40
  } catch (error) {
30
41
  toast.error(t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)));
31
42
  throw error;
32
43
  }
33
- };
44
+ }, [queryClient]);
34
45
  }
35
46
 
36
47
  export function useChatSessionLabel() {
@@ -296,6 +296,10 @@
296
296
  "chatSlashCommandCompactContextDetail": "Keeps the latest turn and the active task available for continuation.",
297
297
  "chatSlashCommandCompactContextSuccess": "Context compacted",
298
298
  "chatSlashCommandCompactContextFailed": "Couldn't compact context",
299
+ "chatSlashCommandUpdateSessionTitleTitle": "Update session title",
300
+ "chatSlashCommandUpdateSessionTitleDescription": "Ask AI to create a concise title from this conversation and update the session.",
301
+ "chatSlashCommandUpdateSessionTitleDetail": "Sends a message automatically without replacing your current draft.",
302
+ "chatSlashCommandUpdateSessionTitlePrompt": "Create a concise, accurate title based on this conversation, then use the sessions_update tool to update the current session title now. Do not only suggest a title.",
299
303
  "chatSlashPanelAppHint": "Press Enter to open this panel app on the right.",
300
304
  "chatSlashSkillHint": "Press Enter to add this skill for the next turn.",
301
305
  "chatPanelAppReferenceLoading": "Loading panel apps...",
@@ -296,6 +296,10 @@
296
296
  "chatSlashCommandCompactContextDetail": "保留最近一轮和当前任务,供后续继续处理。",
297
297
  "chatSlashCommandCompactContextSuccess": "上下文已压缩",
298
298
  "chatSlashCommandCompactContextFailed": "无法压缩上下文",
299
+ "chatSlashCommandUpdateSessionTitleTitle": "更新会话标题",
300
+ "chatSlashCommandUpdateSessionTitleDescription": "让 AI 根据当前会话内容生成简洁标题并完成更新。",
301
+ "chatSlashCommandUpdateSessionTitleDetail": "命令会自动发送一条消息,不会覆盖输入框中的草稿。",
302
+ "chatSlashCommandUpdateSessionTitlePrompt": "请根据当前会话内容生成一个简洁、准确的标题,然后立即使用 sessions_update 工具更新当前会话标题。不要只给出标题建议。",
299
303
  "chatSlashPanelAppHint": "回车在右侧打开这个面板应用。",
300
304
  "chatSlashSkillHint": "回车把该技能加入本轮请求。",
301
305
  "chatPanelAppReferenceLoading": "加载面板应用中…",
@@ -1 +0,0 @@
1
- import{t as e}from"./doc-browser-CbCBDdap.js";export{e as DocBrowser};