@nextclaw/ui 0.15.5 → 0.15.7

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 (169) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/api-V8m62-13.js +13 -0
  3. package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
  10. package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
  12. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
  13. package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
  20. package/dist/assets/index-DtWf7kk9.css +1 -0
  21. package/dist/assets/index-UnY4HtVa.js +2 -0
  22. package/dist/assets/loader-circle-BRSfaztA.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
  27. package/dist/assets/plus-BgMBV9HJ.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
  30. package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
  33. package/dist/assets/remote-C1y3ARHu.js +1 -0
  34. package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
  35. package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
  40. package/dist/assets/security-config-D8me3pXl.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
  48. package/dist/assets/use-config-CY4uF2Ou.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
  51. package/dist/assets/x-C6cM8wkT.js +1 -0
  52. package/dist/index.html +30 -29
  53. package/index.html +1 -0
  54. package/package.json +8 -8
  55. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
  56. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  58. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  59. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  60. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  62. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  65. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  67. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  68. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
  69. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
  71. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  72. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
  73. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
  74. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  75. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  76. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
  77. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  78. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  79. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
  80. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  81. package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
  82. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
  83. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
  84. package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
  85. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  86. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
  87. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  88. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  89. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  93. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  94. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  95. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
  96. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  97. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  98. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  99. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  100. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  101. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  102. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
  104. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
  106. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  108. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
  109. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
  110. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
  112. package/src/features/marketplace/components/marketplace-page.tsx +10 -2
  113. package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
  114. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
  115. package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
  116. package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
  117. package/src/features/settings/components/config/provider-form.tsx +0 -4
  118. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  119. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  120. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  121. package/src/features/system-status/index.ts +9 -0
  122. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  123. package/src/index.css +10 -16
  124. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  125. package/src/shared/components/common/brand-header.tsx +121 -35
  126. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  127. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  128. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  129. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  130. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  131. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  132. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  133. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  134. package/src/shared/hooks/use-projects.ts +28 -0
  135. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  136. package/src/shared/hooks/use-server-path-search.ts +40 -0
  137. package/src/shared/lib/api/index.ts +1 -0
  138. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  139. package/src/shared/lib/api/types.ts +27 -0
  140. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  143. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
  144. package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
  145. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
  147. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  148. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
  149. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
  150. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  151. package/dist/assets/api-IlX_CbwU.js +0 -13
  152. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  153. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  154. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  155. package/dist/assets/index-0av6WO9-.css +0 -1
  156. package/dist/assets/index-CT0iYlxV.js +0 -2
  157. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  158. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  159. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  160. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  161. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  162. package/dist/assets/remote-BaVECEZw.js +0 -1
  163. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  164. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  165. package/dist/assets/security-config-BPzet76z.js +0 -1
  166. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  167. package/dist/assets/x-B3o1dWR5.js +0 -1
  168. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  169. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -0,0 +1,55 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { useSessionConversationInputState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
5
+ import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
6
+
7
+ const MODEL_OPTIONS: ChatModelOption[] = [
8
+ {
9
+ value: 'deepseek/deepseek-v4-flash',
10
+ modelLabel: 'DeepSeek V4 Flash',
11
+ providerLabel: 'DeepSeek',
12
+ thinkingCapability: null,
13
+ },
14
+ {
15
+ value: 'minimax/MiniMax-M3',
16
+ modelLabel: 'MiniMax M3',
17
+ providerLabel: 'MiniMax',
18
+ thinkingCapability: null,
19
+ },
20
+ ];
21
+
22
+ describe('useSessionConversationInputState session preferences', () => {
23
+ it('restores the switched session model after its metadata becomes available', () => {
24
+ const { result } = renderHook(() => useSessionConversationInputState());
25
+
26
+ act(() => {
27
+ result.current.inputActions.syncSessionPreferences({
28
+ modelOptions: MODEL_OPTIONS,
29
+ selectedSessionExists: true,
30
+ selectedSessionKey: 'session-a',
31
+ selectedSessionPreferredModel: 'deepseek/deepseek-v4-flash',
32
+ });
33
+ });
34
+ expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
35
+
36
+ act(() => {
37
+ result.current.inputActions.syncSessionPreferences({
38
+ modelOptions: MODEL_OPTIONS,
39
+ selectedSessionExists: false,
40
+ selectedSessionKey: 'session-b',
41
+ });
42
+ });
43
+ expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
44
+
45
+ act(() => {
46
+ result.current.inputActions.syncSessionPreferences({
47
+ modelOptions: MODEL_OPTIONS,
48
+ selectedSessionExists: true,
49
+ selectedSessionKey: 'session-b',
50
+ selectedSessionPreferredModel: 'minimax/MiniMax-M3',
51
+ });
52
+ });
53
+ expect(result.current.inputSnapshot.selectedModel).toBe('minimax/MiniMax-M3');
54
+ });
55
+ });
@@ -5,6 +5,10 @@ import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
5
5
  import type { ThinkingLevel } from '@/shared/lib/api';
6
6
  import { DEFAULT_SESSION_TYPE } from '@/features/chat/features/session-type/utils/chat-session-type.utils';
7
7
  import { createChatComposerNodesFromDraft } from '@/features/chat/features/input/utils/chat-composer-state.utils';
8
+ import {
9
+ useSessionConversationPreferenceActions,
10
+ type SessionConversationPreferenceSyncParams,
11
+ } from '@/features/chat/features/conversation/hooks/use-session-conversation-preference-actions';
8
12
 
9
13
  type SessionConversationInputStateValue = string | null | undefined;
10
14
  type SessionConversationSkillSelection = {
@@ -51,6 +55,7 @@ export type SessionConversationInputActions = {
51
55
  readonly removeAttachment: (attachmentId: string) => void;
52
56
  readonly setSelectedModel: (model: SessionConversationInputStateValue) => void;
53
57
  readonly setSelectedThinkingLevel: (level: ThinkingLevel | null) => void;
58
+ readonly syncSessionPreferences: (params: SessionConversationPreferenceSyncParams) => void;
54
59
  readonly setPendingSessionType: (sessionType: SetStateAction<string>) => void;
55
60
  readonly setPendingProjectRoot: (projectRoot: string | null) => void;
56
61
  readonly setSelectedSkills: (
@@ -198,13 +203,11 @@ export const useSessionConversationInputState = () => {
198
203
  }));
199
204
  }, [update]);
200
205
 
201
- const setSelectedModel = useCallback((model: SessionConversationInputStateValue) => {
202
- update({ selectedModel: model });
203
- }, [update]);
204
-
205
- const setSelectedThinkingLevel = useCallback((level: ThinkingLevel | null) => {
206
- update({ selectedThinkingLevel: level });
207
- }, [update]);
206
+ const preferenceActions = useSessionConversationPreferenceActions({
207
+ selectedModel: snapshot.selectedModel,
208
+ selectedThinkingLevel: snapshot.selectedThinkingLevel,
209
+ updatePreferences: update,
210
+ });
208
211
 
209
212
  const setPendingSessionType = useCallback((sessionType: SetStateAction<string>) => {
210
213
  update((current) => {
@@ -249,8 +252,7 @@ export const useSessionConversationInputState = () => {
249
252
  setAttachments,
250
253
  addAttachments,
251
254
  removeAttachment,
252
- setSelectedModel,
253
- setSelectedThinkingLevel,
255
+ ...preferenceActions,
254
256
  setPendingSessionType,
255
257
  setPendingProjectRoot,
256
258
  setSelectedSkills,
@@ -266,8 +268,7 @@ export const useSessionConversationInputState = () => {
266
268
  setAttachments,
267
269
  addAttachments,
268
270
  removeAttachment,
269
- setSelectedModel,
270
- setSelectedThinkingLevel,
271
+ preferenceActions,
271
272
  setPendingSessionType,
272
273
  setPendingProjectRoot,
273
274
  setSelectedSkills,
@@ -0,0 +1,94 @@
1
+ import { useCallback, useMemo, useRef } from 'react';
2
+
3
+ import type { ThinkingLevel } from '@/shared/lib/api';
4
+ import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
5
+ import {
6
+ resolveSelectedModelValue,
7
+ resolveSelectedThinkingLevelValue,
8
+ } from '@/features/chat/features/session/utils/chat-session-preference-governance.utils';
9
+
10
+ type ConversationPreferenceValue = string | null | undefined;
11
+
12
+ export type SessionConversationPreferenceSyncParams = {
13
+ readonly defaultModel?: string;
14
+ readonly fallbackPreferredModel?: string;
15
+ readonly fallbackPreferredThinking?: ThinkingLevel | null;
16
+ readonly modelOptions: ChatModelOption[];
17
+ readonly selectedSessionExists: boolean;
18
+ readonly selectedSessionKey?: string | null;
19
+ readonly selectedSessionPreferredModel?: string;
20
+ readonly selectedSessionPreferredThinking?: ThinkingLevel | null;
21
+ };
22
+
23
+ type SessionConversationPreferenceActionsParams = {
24
+ readonly selectedModel: ConversationPreferenceValue;
25
+ readonly selectedThinkingLevel: ThinkingLevel | null;
26
+ readonly updatePreferences: (patch: {
27
+ readonly selectedModel?: ConversationPreferenceValue;
28
+ readonly selectedThinkingLevel?: ThinkingLevel | null;
29
+ }) => void;
30
+ };
31
+
32
+ export const useSessionConversationPreferenceActions = ({
33
+ selectedModel: currentSelectedModel,
34
+ selectedThinkingLevel: currentSelectedThinkingLevel,
35
+ updatePreferences,
36
+ }: SessionConversationPreferenceActionsParams) => {
37
+ const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
38
+ const setSelectedModel = useCallback((selectedModel: ConversationPreferenceValue) => {
39
+ updatePreferences({ selectedModel });
40
+ }, [updatePreferences]);
41
+ const setSelectedThinkingLevel = useCallback((selectedThinkingLevel: ThinkingLevel | null) => {
42
+ updatePreferences({ selectedThinkingLevel });
43
+ }, [updatePreferences]);
44
+ const syncSessionPreferences = useCallback(({
45
+ defaultModel,
46
+ fallbackPreferredModel,
47
+ fallbackPreferredThinking,
48
+ modelOptions,
49
+ selectedSessionExists,
50
+ selectedSessionKey: sessionKey,
51
+ selectedSessionPreferredModel,
52
+ selectedSessionPreferredThinking,
53
+ }: SessionConversationPreferenceSyncParams) => {
54
+ const selectedSessionKey = sessionKey ?? null;
55
+ const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
56
+ const preserveCurrentPreference = sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists;
57
+ const selectedModel = resolveSelectedModelValue({
58
+ currentSelectedModel: currentSelectedModel ?? undefined,
59
+ modelOptions,
60
+ selectedSessionPreferredModel,
61
+ fallbackPreferredModel,
62
+ defaultModel,
63
+ preferSessionPreferredModel: sessionChanged,
64
+ preserveCurrentSelectedModelOnSessionChange: preserveCurrentPreference,
65
+ });
66
+ const modelOption = modelOptions.find((option) => option.value === selectedModel);
67
+ const selectedThinkingLevel = resolveSelectedThinkingLevelValue({
68
+ currentSelectedThinkingLevel,
69
+ supportedThinkingLevels:
70
+ (modelOption?.thinkingCapability?.supported as ThinkingLevel[] | undefined) ?? [],
71
+ selectedSessionPreferredThinking,
72
+ fallbackPreferredThinking,
73
+ defaultThinkingLevel:
74
+ (modelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
75
+ preferSessionPreferredThinking: sessionChanged,
76
+ preserveCurrentSelectedThinkingOnSessionChange: preserveCurrentPreference,
77
+ });
78
+ if (
79
+ currentSelectedModel !== selectedModel ||
80
+ currentSelectedThinkingLevel !== selectedThinkingLevel
81
+ ) {
82
+ updatePreferences({ selectedModel, selectedThinkingLevel });
83
+ }
84
+ if (!selectedSessionKey || selectedSessionExists) {
85
+ previousSessionKeyRef.current = selectedSessionKey;
86
+ }
87
+ }, [currentSelectedModel, currentSelectedThinkingLevel, updatePreferences]);
88
+
89
+ return useMemo(() => ({
90
+ setSelectedModel,
91
+ setSelectedThinkingLevel,
92
+ syncSessionPreferences,
93
+ }), [setSelectedModel, setSelectedThinkingLevel, syncSessionPreferences]);
94
+ };
@@ -9,10 +9,41 @@ const usePanelAppsMock = vi.hoisted(() =>
9
9
  isLoading: false,
10
10
  })),
11
11
  );
12
+ const useServerPathSearchMock = vi.hoisted(() =>
13
+ vi.fn(() => ({
14
+ data: { entries: [] },
15
+ error: null,
16
+ isFetching: false,
17
+ isLoading: false,
18
+ })),
19
+ );
20
+ const useServerPathBrowseMock = vi.hoisted(() =>
21
+ vi.fn(() => ({
22
+ data: {
23
+ entries: [
24
+ {
25
+ name: 'src',
26
+ path: '/tmp/project/src',
27
+ kind: 'directory' as const,
28
+ hidden: false,
29
+ },
30
+ ],
31
+ },
32
+ error: null,
33
+ isFetching: false,
34
+ isLoading: false,
35
+ })),
36
+ );
12
37
 
13
38
  vi.mock('@/features/panel-apps', () => ({
14
39
  usePanelApps: usePanelAppsMock,
15
40
  }));
41
+ vi.mock('@/shared/hooks/use-server-path-search', () => ({
42
+ useServerPathSearch: useServerPathSearchMock,
43
+ }));
44
+ vi.mock('@/shared/hooks/use-server-path-browse', () => ({
45
+ useServerPathBrowse: useServerPathBrowseMock,
46
+ }));
16
47
 
17
48
  function createHookParams() {
18
49
  return {
@@ -29,6 +60,7 @@ function createHookParams() {
29
60
  language: 'zh' as const,
30
61
  onSelectPanelApp: vi.fn(),
31
62
  onSelectSkill: vi.fn(),
63
+ projectRoot: '/tmp/project',
32
64
  recentSkillValues: [],
33
65
  skillRecords: [],
34
66
  };
@@ -36,6 +68,56 @@ function createHookParams() {
36
68
 
37
69
  beforeEach(() => {
38
70
  usePanelAppsMock.mockClear();
71
+ useServerPathBrowseMock.mockClear();
72
+ useServerPathSearchMock.mockClear();
73
+ });
74
+
75
+ it('browses nested folders while reserving search for project-wide queries', () => {
76
+ const { result } = renderHook(() => useChatInputSurfaceState(createHookParams()));
77
+
78
+ act(() =>
79
+ result.current.setInputSurfaceTrigger({
80
+ end: 1,
81
+ key: 'context-reference',
82
+ marker: '@',
83
+ query: '',
84
+ start: 0,
85
+ }),
86
+ );
87
+ const filesItem = result.current.inputSurfaceState.panel?.items.find(
88
+ (item) => item.selectionBehavior === 'navigate',
89
+ );
90
+ act(() => result.current.inputSurfaceState.panel?.onSelectItem?.(filesItem!));
91
+
92
+ expect(useServerPathBrowseMock).toHaveBeenLastCalledWith({
93
+ path: '.',
94
+ basePath: '/tmp/project',
95
+ includeFiles: true,
96
+ enabled: true,
97
+ });
98
+ const directoryItem = result.current.inputSurfaceState.panel?.items.find(
99
+ (item) => item.title === 'src',
100
+ );
101
+ act(() => result.current.inputSurfaceState.panel?.onSelectItem?.(directoryItem!));
102
+ expect(useServerPathBrowseMock).toHaveBeenLastCalledWith({
103
+ path: 'src',
104
+ basePath: '/tmp/project',
105
+ includeFiles: true,
106
+ enabled: true,
107
+ });
108
+
109
+ act(() => result.current.setInputSurfaceTrigger({
110
+ end: 7,
111
+ key: 'context-reference',
112
+ marker: '@',
113
+ query: 'needle',
114
+ start: 0,
115
+ }));
116
+ expect(useServerPathSearchMock).toHaveBeenLastCalledWith({
117
+ basePath: '/tmp/project',
118
+ query: 'needle',
119
+ enabled: true,
120
+ });
39
121
  });
40
122
 
41
123
  it('keeps equivalent input surface trigger updates from rerendering the composer owner', () => {
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useRef, useState } from 'react';
1
+ import { useCallback, useDeferredValue, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC,
4
4
  resolveChatInputSurfaceState,
@@ -7,105 +7,216 @@ import {
7
7
  import { usePanelApps } from '@/features/panel-apps';
8
8
  import { t, type I18nLanguage } from '@/shared/lib/i18n';
9
9
  import {
10
- createPanelAppReferenceInputSurfacePlugin,
11
- PANEL_APP_REFERENCE_TRIGGER_SPEC,
12
- } from '@/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils';
10
+ CONTEXT_REFERENCE_TRIGGER_SPEC,
11
+ createContextReferenceInputSurfacePlugin,
12
+ } from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
13
13
  import {
14
14
  createSlashCommandInputSurfacePlugin,
15
15
  type ChatSlashCommandDescriptor,
16
16
  } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
17
17
  import type { ChatInputBarAdapterTexts, ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
18
+ import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
19
+ import { useServerPathSearch } from '@/shared/hooks/use-server-path-search';
20
+ import type { ServerPathEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
18
21
 
19
- export function useChatInputSurfaceState(params: {
22
+ type ChatInputSurfaceSlashTexts = Pick<
23
+ ChatInputBarAdapterTexts,
24
+ 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
25
+ >;
26
+
27
+ type UseChatInputSurfaceStateParams = {
20
28
  commands: readonly ChatSlashCommandDescriptor[];
21
29
  isSkillsLoading: boolean;
22
- itemTexts: {
23
- slashTexts: Pick<
24
- ChatInputBarAdapterTexts,
25
- 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
26
- >;
27
- };
30
+ itemTexts: { slashTexts: ChatInputSurfaceSlashTexts };
28
31
  language: I18nLanguage;
29
32
  onSelectPanelApp: (appId: string) => void;
30
33
  onSelectSkill: (skillRef: string) => void;
34
+ projectRoot: string;
31
35
  recentSkillValues: readonly string[];
32
36
  skillRecords: readonly ChatSkillRecord[];
37
+ };
38
+
39
+ function buildChatInputSurfacePlugins(params: {
40
+ commands: readonly ChatSlashCommandDescriptor[];
41
+ language: I18nLanguage;
42
+ onNavigate: (path: string | null) => void;
43
+ onSelectPanelApp: (appId: string) => void;
44
+ onSelectSkill: (skillRef: string) => void;
45
+ slashTexts: ChatInputSurfaceSlashTexts;
33
46
  }) {
47
+ const {
48
+ commands,
49
+ language,
50
+ onNavigate,
51
+ onSelectPanelApp,
52
+ onSelectSkill,
53
+ slashTexts,
54
+ } = params;
55
+ const panelAppTexts = {
56
+ appIdLabel: t('chatPanelAppReferenceAppId', language),
57
+ fileLabel: t('chatPanelAppReferenceFile', language),
58
+ noDescriptionLabel: t('chatPanelAppReferenceNoDescription', language),
59
+ subtitle: t('chatPanelAppReferenceType', language),
60
+ };
61
+ return [
62
+ createSlashCommandInputSurfacePlugin({
63
+ commands,
64
+ itemTexts: {
65
+ panelAppTexts,
66
+ skillTexts: slashTexts,
67
+ },
68
+ menuTexts: {
69
+ loadingLabel: t('chatSlashLoading', language),
70
+ sectionLabel: t('chatSlashSection', language),
71
+ emptyLabel: t('chatSlashNoResult', language),
72
+ hintLabel: t('chatSlashHint', language),
73
+ itemHintLabel: t('chatSlashSkillHint', language),
74
+ },
75
+ labels: {
76
+ commandHintLabel: t('chatSlashCommandHint', language),
77
+ commandSectionLabel: t('chatSlashSectionCommands', language),
78
+ commandSubtitle: t('chatSlashTypeCommand', language),
79
+ filterAllLabel: t('chatSlashFilterAll', language),
80
+ filterCommandsLabel: t('chatSlashFilterCommands', language),
81
+ filterPanelAppsLabel: t('chatSlashFilterPanelApps', language),
82
+ filterSkillsLabel: t('chatSlashFilterSkills', language),
83
+ panelAppHintLabel: t('chatSlashPanelAppHint', language),
84
+ panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
85
+ skillHintLabel: t('chatSlashSkillHint', language),
86
+ skillSectionLabel: t('chatSlashSectionSkills', language),
87
+ },
88
+ onSelectPanelApp,
89
+ onSelectSkill,
90
+ }),
91
+ createContextReferenceInputSurfacePlugin({
92
+ itemTexts: {
93
+ context: {
94
+ backLabel: t('chatContextReferenceBack', language),
95
+ backDescription: t('chatContextReferenceBackDescription', language),
96
+ backHintLabel: t('chatContextReferenceBackHint', language),
97
+ currentDirectoryLabel: t('chatContextReferenceCurrentDirectory', language),
98
+ directoryDescription: t('chatContextReferenceDirectoryDescription', language),
99
+ fileDescription: t('chatContextReferenceFileDescription', language),
100
+ filesDescription: t('chatContextReferenceFilesDescription', language),
101
+ filesHintLabel: t('chatContextReferenceFilesHint', language),
102
+ filesLabel: t('chatContextReferenceFilesLabel', language),
103
+ filesSubtitle: t('chatContextReferenceFilesType', language),
104
+ panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
105
+ parentLabel: t('chatContextReferenceParent', language),
106
+ parentDescription: t('chatContextReferenceParentDescription', language),
107
+ parentHintLabel: t('chatContextReferenceParentHint', language),
108
+ projectRootLabel: t('chatContextReferenceProjectRoot', language),
109
+ searchFailedLabel: t('chatContextReferenceSearchFailed', language),
110
+ workspaceSectionLabel: t('chatContextReferenceWorkspaceSection', language),
111
+ },
112
+ panelApp: panelAppTexts,
113
+ },
114
+ menuTexts: {
115
+ loadingLabel: t('chatContextReferenceLoading', language),
116
+ sectionLabel: t('chatContextReferenceSection', language),
117
+ emptyLabel: t('chatContextReferenceNoResult', language),
118
+ hintLabel: t('chatContextReferenceHint', language),
119
+ itemHintLabel: t('chatContextReferenceItemHint', language),
120
+ },
121
+ onNavigate,
122
+ }),
123
+ ];
124
+ }
125
+
126
+ function buildBrowsedReferenceEntries(params: {
127
+ entries: readonly ServerPathEntryView[];
128
+ parentRelativePath: string;
129
+ }): ServerPathSearchEntryView[] {
130
+ const parentRelativePath = params.parentRelativePath.split('/').filter(Boolean).join('/');
131
+ return params.entries.map((entry) => ({
132
+ name: entry.name,
133
+ path: entry.path,
134
+ relativePath: [parentRelativePath, entry.name].filter(Boolean).join('/'),
135
+ parentRelativePath,
136
+ kind: entry.kind,
137
+ hidden: entry.hidden,
138
+ }));
139
+ }
140
+
141
+ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams) {
34
142
  const {
35
143
  isSkillsLoading,
36
144
  itemTexts,
37
145
  language,
38
146
  onSelectPanelApp,
39
147
  onSelectSkill,
148
+ projectRoot,
40
149
  recentSkillValues,
41
150
  skillRecords,
42
151
  commands,
43
152
  } = params;
153
+ const [referencePath, setReferencePath] = useState<string | null>(null);
44
154
  const [inputSurfaceTrigger, setInputSurfaceTriggerState] = useState<ChatInputSurfaceTrigger | null>(null);
45
155
  const inputSurfaceTriggerSignatureRef = useRef('null');
46
156
  const setInputSurfaceTrigger = useCallback((nextTrigger: ChatInputSurfaceTrigger | null): void => {
47
157
  const nextSignature = JSON.stringify(nextTrigger ? [nextTrigger.key, nextTrigger.marker, nextTrigger.query, nextTrigger.start, nextTrigger.end] : null);
48
158
  if (inputSurfaceTriggerSignatureRef.current === nextSignature) return;
49
159
  inputSurfaceTriggerSignatureRef.current = nextSignature;
160
+ if (!nextTrigger) {
161
+ setReferencePath(null);
162
+ }
50
163
  setInputSurfaceTriggerState(nextTrigger);
51
164
  }, []);
165
+ const isContextReferenceTrigger = inputSurfaceTrigger?.key === CONTEXT_REFERENCE_TRIGGER_SPEC.key;
166
+ const deferredReferenceQuery = useDeferredValue(
167
+ isContextReferenceTrigger ? inputSurfaceTrigger.query : '',
168
+ );
169
+ const isBrowsingServerPaths = Boolean(
170
+ isContextReferenceTrigger &&
171
+ projectRoot &&
172
+ referencePath !== null &&
173
+ !deferredReferenceQuery.trim(),
174
+ );
175
+ const shouldSearchServerPaths = Boolean(
176
+ isContextReferenceTrigger &&
177
+ projectRoot &&
178
+ deferredReferenceQuery.trim(),
179
+ );
180
+ const serverPathBrowse = useServerPathBrowse({
181
+ path: referencePath || '.',
182
+ basePath: projectRoot,
183
+ includeFiles: true,
184
+ enabled: isBrowsingServerPaths,
185
+ });
186
+ const serverPathSearch = useServerPathSearch({
187
+ basePath: projectRoot,
188
+ query: deferredReferenceQuery,
189
+ enabled: shouldSearchServerPaths,
190
+ });
191
+ const activeServerPathQuery = isBrowsingServerPaths ? serverPathBrowse : serverPathSearch;
192
+ const serverPathEntries = useMemo(
193
+ () => isBrowsingServerPaths
194
+ ? buildBrowsedReferenceEntries({
195
+ entries: serverPathBrowse.data?.entries ?? [],
196
+ parentRelativePath: referencePath ?? '',
197
+ })
198
+ : serverPathSearch.data?.entries ?? [],
199
+ [
200
+ isBrowsingServerPaths,
201
+ referencePath,
202
+ serverPathBrowse.data?.entries,
203
+ serverPathSearch.data?.entries,
204
+ ],
205
+ );
52
206
  const shouldLoadPanelApps =
53
- inputSurfaceTrigger?.key === PANEL_APP_REFERENCE_TRIGGER_SPEC.key ||
207
+ isContextReferenceTrigger ||
54
208
  inputSurfaceTrigger?.key === CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC.key;
55
209
  const panelApps = usePanelApps({ enabled: shouldLoadPanelApps });
56
210
 
57
211
  const inputSurfacePlugins = useMemo(
58
- () => [
59
- createSlashCommandInputSurfacePlugin({
60
- commands,
61
- itemTexts: {
62
- panelAppTexts: {
63
- appIdLabel: t('chatPanelAppReferenceAppId', language),
64
- fileLabel: t('chatPanelAppReferenceFile', language),
65
- noDescriptionLabel: t('chatPanelAppReferenceNoDescription', language),
66
- subtitle: t('chatPanelAppReferenceType', language),
67
- },
68
- skillTexts: itemTexts.slashTexts,
69
- },
70
- menuTexts: {
71
- loadingLabel: t('chatSlashLoading', language),
72
- sectionLabel: t('chatSlashSection', language),
73
- emptyLabel: t('chatSlashNoResult', language),
74
- hintLabel: t('chatSlashHint', language),
75
- itemHintLabel: t('chatSlashSkillHint', language)
76
- },
77
- labels: {
78
- commandHintLabel: t('chatSlashCommandHint', language),
79
- commandSectionLabel: t('chatSlashSectionCommands', language),
80
- commandSubtitle: t('chatSlashTypeCommand', language),
81
- filterAllLabel: t('chatSlashFilterAll', language),
82
- filterCommandsLabel: t('chatSlashFilterCommands', language),
83
- filterPanelAppsLabel: t('chatSlashFilterPanelApps', language),
84
- filterSkillsLabel: t('chatSlashFilterSkills', language),
85
- panelAppHintLabel: t('chatSlashPanelAppHint', language),
86
- panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
87
- skillHintLabel: t('chatSlashSkillHint', language),
88
- skillSectionLabel: t('chatSlashSectionSkills', language)
89
- },
90
- onSelectPanelApp,
91
- onSelectSkill
92
- }),
93
- createPanelAppReferenceInputSurfacePlugin({
94
- itemTexts: {
95
- appIdLabel: t('chatPanelAppReferenceAppId', language),
96
- fileLabel: t('chatPanelAppReferenceFile', language),
97
- noDescriptionLabel: t('chatPanelAppReferenceNoDescription', language),
98
- subtitle: t('chatPanelAppReferenceType', language)
99
- },
100
- menuTexts: {
101
- loadingLabel: t('chatPanelAppReferenceLoading', language),
102
- sectionLabel: t('chatPanelAppReferenceSection', language),
103
- emptyLabel: t('chatPanelAppReferenceNoResult', language),
104
- hintLabel: t('chatPanelAppReferenceHint', language),
105
- itemHintLabel: t('chatPanelAppReferenceItemHint', language)
106
- }
107
- })
108
- ],
212
+ () => buildChatInputSurfacePlugins({
213
+ commands,
214
+ language,
215
+ onNavigate: setReferencePath,
216
+ onSelectPanelApp,
217
+ onSelectSkill,
218
+ slashTexts: itemTexts.slashTexts,
219
+ }),
109
220
  [
110
221
  commands,
111
222
  itemTexts.slashTexts,
@@ -118,9 +229,16 @@ export function useChatInputSurfaceState(params: {
118
229
  () => resolveChatInputSurfaceState({
119
230
  data: {
120
231
  isPanelAppsLoading: panelApps.isLoading || panelApps.isFetching,
232
+ isServerPathSearchLoading: activeServerPathQuery.isLoading || activeServerPathQuery.isFetching,
121
233
  isSkillsLoading,
122
234
  panelApps: panelApps.data?.entries ?? [],
235
+ projectRoot,
123
236
  recentSkillValues,
237
+ referencePath,
238
+ serverPathEntries,
239
+ serverPathSearchError: activeServerPathQuery.error instanceof Error
240
+ ? activeServerPathQuery.error.message
241
+ : null,
124
242
  skillRecords
125
243
  },
126
244
  plugins: inputSurfacePlugins,
@@ -132,6 +250,12 @@ export function useChatInputSurfaceState(params: {
132
250
  panelApps.data?.entries,
133
251
  panelApps.isFetching,
134
252
  panelApps.isLoading,
253
+ projectRoot,
254
+ activeServerPathQuery.error,
255
+ activeServerPathQuery.isFetching,
256
+ activeServerPathQuery.isLoading,
257
+ referencePath,
258
+ serverPathEntries,
135
259
  isSkillsLoading,
136
260
  recentSkillValues,
137
261
  skillRecords