@nextclaw/ui 0.15.5 → 0.15.6

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 (147) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/api-Dyb5rgh0.js +13 -0
  3. package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
  10. package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
  12. package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
  13. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
  20. package/dist/assets/index-BVn702m4.css +1 -0
  21. package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
  22. package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
  27. package/dist/assets/plus-TmFdwO3P.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
  30. package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
  33. package/dist/assets/remote-B9F2uCk9.js +1 -0
  34. package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
  35. package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
  40. package/dist/assets/security-config-Cr2ERKLu.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
  48. package/dist/assets/use-config-CwDEAl6f.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
  51. package/dist/assets/x-DdEDL6ju.js +1 -0
  52. package/dist/index.html +29 -29
  53. package/package.json +8 -8
  54. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
  55. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  59. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
  62. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  63. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  64. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  67. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
  68. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
  69. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  71. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
  72. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
  73. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  74. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  75. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
  76. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  77. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  78. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
  79. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  80. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
  81. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  82. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  83. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  84. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  85. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  86. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  87. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  88. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  89. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
  90. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  91. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  92. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  93. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  94. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  95. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  96. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  97. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  98. package/src/features/settings/components/config/provider-form.tsx +0 -4
  99. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  100. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  101. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  102. package/src/features/system-status/index.ts +9 -0
  103. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  104. package/src/index.css +10 -16
  105. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  106. package/src/shared/components/common/brand-header.tsx +121 -35
  107. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  108. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  109. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  110. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  111. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  112. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  113. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  114. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  115. package/src/shared/hooks/use-projects.ts +28 -0
  116. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  117. package/src/shared/hooks/use-server-path-search.ts +40 -0
  118. package/src/shared/lib/api/index.ts +1 -0
  119. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  120. package/src/shared/lib/api/types.ts +27 -0
  121. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  122. package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
  123. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  124. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
  125. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
  127. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  128. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
  129. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  130. package/dist/assets/api-IlX_CbwU.js +0 -13
  131. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  132. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  133. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  134. package/dist/assets/index-0av6WO9-.css +0 -1
  135. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  136. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  137. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  138. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  139. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  140. package/dist/assets/remote-BaVECEZw.js +0 -1
  141. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  142. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  143. package/dist/assets/security-config-BPzet76z.js +0 -1
  144. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  145. package/dist/assets/x-B3o1dWR5.js +0 -1
  146. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  147. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -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,21 @@ 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
+ );
12
20
 
13
21
  vi.mock('@/features/panel-apps', () => ({
14
22
  usePanelApps: usePanelAppsMock,
15
23
  }));
24
+ vi.mock('@/shared/hooks/use-server-path-search', () => ({
25
+ useServerPathSearch: useServerPathSearchMock,
26
+ }));
16
27
 
17
28
  function createHookParams() {
18
29
  return {
@@ -29,6 +40,7 @@ function createHookParams() {
29
40
  language: 'zh' as const,
30
41
  onSelectPanelApp: vi.fn(),
31
42
  onSelectSkill: vi.fn(),
43
+ projectRoot: '/tmp/project',
32
44
  recentSkillValues: [],
33
45
  skillRecords: [],
34
46
  };
@@ -36,6 +48,31 @@ function createHookParams() {
36
48
 
37
49
  beforeEach(() => {
38
50
  usePanelAppsMock.mockClear();
51
+ useServerPathSearchMock.mockClear();
52
+ });
53
+
54
+ it('enables project path search after navigating into files and folders', () => {
55
+ const { result } = renderHook(() => useChatInputSurfaceState(createHookParams()));
56
+
57
+ act(() =>
58
+ result.current.setInputSurfaceTrigger({
59
+ end: 1,
60
+ key: 'context-reference',
61
+ marker: '@',
62
+ query: '',
63
+ start: 0,
64
+ }),
65
+ );
66
+ const filesItem = result.current.inputSurfaceState.panel?.items.find(
67
+ (item) => item.selectionBehavior === 'navigate',
68
+ );
69
+ act(() => result.current.inputSurfaceState.panel?.onSelectItem?.(filesItem!));
70
+
71
+ expect(useServerPathSearchMock).toHaveBeenLastCalledWith({
72
+ basePath: '/tmp/project',
73
+ query: '',
74
+ enabled: true,
75
+ });
39
76
  });
40
77
 
41
78
  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,169 @@ 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
+ import type { ContextReferenceMode } from '@/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types';
13
14
  import {
14
15
  createSlashCommandInputSurfacePlugin,
15
16
  type ChatSlashCommandDescriptor,
16
17
  } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
17
18
  import type { ChatInputBarAdapterTexts, ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
19
+ import { useServerPathSearch } from '@/shared/hooks/use-server-path-search';
18
20
 
19
- export function useChatInputSurfaceState(params: {
21
+ type ChatInputSurfaceSlashTexts = Pick<
22
+ ChatInputBarAdapterTexts,
23
+ 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
24
+ >;
25
+
26
+ type UseChatInputSurfaceStateParams = {
20
27
  commands: readonly ChatSlashCommandDescriptor[];
21
28
  isSkillsLoading: boolean;
22
- itemTexts: {
23
- slashTexts: Pick<
24
- ChatInputBarAdapterTexts,
25
- 'slashSkillSubtitle' | 'slashSkillSpecLabel' | 'slashSkillScopeLabel' | 'noSkillDescription'
26
- >;
27
- };
29
+ itemTexts: { slashTexts: ChatInputSurfaceSlashTexts };
28
30
  language: I18nLanguage;
29
31
  onSelectPanelApp: (appId: string) => void;
30
32
  onSelectSkill: (skillRef: string) => void;
33
+ projectRoot: string;
31
34
  recentSkillValues: readonly string[];
32
35
  skillRecords: readonly ChatSkillRecord[];
36
+ };
37
+
38
+ function buildChatInputSurfacePlugins(params: {
39
+ commands: readonly ChatSlashCommandDescriptor[];
40
+ language: I18nLanguage;
41
+ onNavigate: (mode: ContextReferenceMode) => void;
42
+ onSelectPanelApp: (appId: string) => void;
43
+ onSelectSkill: (skillRef: string) => void;
44
+ slashTexts: ChatInputSurfaceSlashTexts;
33
45
  }) {
46
+ const {
47
+ commands,
48
+ language,
49
+ onNavigate,
50
+ onSelectPanelApp,
51
+ onSelectSkill,
52
+ slashTexts,
53
+ } = params;
54
+ const panelAppTexts = {
55
+ appIdLabel: t('chatPanelAppReferenceAppId', language),
56
+ fileLabel: t('chatPanelAppReferenceFile', language),
57
+ noDescriptionLabel: t('chatPanelAppReferenceNoDescription', language),
58
+ subtitle: t('chatPanelAppReferenceType', language),
59
+ };
60
+ return [
61
+ createSlashCommandInputSurfacePlugin({
62
+ commands,
63
+ itemTexts: {
64
+ panelAppTexts,
65
+ skillTexts: slashTexts,
66
+ },
67
+ menuTexts: {
68
+ loadingLabel: t('chatSlashLoading', language),
69
+ sectionLabel: t('chatSlashSection', language),
70
+ emptyLabel: t('chatSlashNoResult', language),
71
+ hintLabel: t('chatSlashHint', language),
72
+ itemHintLabel: t('chatSlashSkillHint', language),
73
+ },
74
+ labels: {
75
+ commandHintLabel: t('chatSlashCommandHint', language),
76
+ commandSectionLabel: t('chatSlashSectionCommands', language),
77
+ commandSubtitle: t('chatSlashTypeCommand', language),
78
+ filterAllLabel: t('chatSlashFilterAll', language),
79
+ filterCommandsLabel: t('chatSlashFilterCommands', language),
80
+ filterPanelAppsLabel: t('chatSlashFilterPanelApps', language),
81
+ filterSkillsLabel: t('chatSlashFilterSkills', language),
82
+ panelAppHintLabel: t('chatSlashPanelAppHint', language),
83
+ panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
84
+ skillHintLabel: t('chatSlashSkillHint', language),
85
+ skillSectionLabel: t('chatSlashSectionSkills', language),
86
+ },
87
+ onSelectPanelApp,
88
+ onSelectSkill,
89
+ }),
90
+ createContextReferenceInputSurfacePlugin({
91
+ itemTexts: {
92
+ context: {
93
+ backLabel: t('chatContextReferenceBack', language),
94
+ backDescription: t('chatContextReferenceBackDescription', language),
95
+ backHintLabel: t('chatContextReferenceBackHint', language),
96
+ directoryDescription: t('chatContextReferenceDirectoryDescription', language),
97
+ fileDescription: t('chatContextReferenceFileDescription', language),
98
+ filesDescription: t('chatContextReferenceFilesDescription', language),
99
+ filesHintLabel: t('chatContextReferenceFilesHint', language),
100
+ filesLabel: t('chatContextReferenceFilesLabel', language),
101
+ filesSubtitle: t('chatContextReferenceFilesType', language),
102
+ panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
103
+ projectRootLabel: t('chatContextReferenceProjectRoot', language),
104
+ searchFailedLabel: t('chatContextReferenceSearchFailed', language),
105
+ workspaceSectionLabel: t('chatContextReferenceWorkspaceSection', language),
106
+ },
107
+ panelApp: panelAppTexts,
108
+ },
109
+ menuTexts: {
110
+ loadingLabel: t('chatContextReferenceLoading', language),
111
+ sectionLabel: t('chatContextReferenceSection', language),
112
+ emptyLabel: t('chatContextReferenceNoResult', language),
113
+ hintLabel: t('chatContextReferenceHint', language),
114
+ itemHintLabel: t('chatContextReferenceItemHint', language),
115
+ },
116
+ onNavigate,
117
+ }),
118
+ ];
119
+ }
120
+
121
+ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams) {
34
122
  const {
35
123
  isSkillsLoading,
36
124
  itemTexts,
37
125
  language,
38
126
  onSelectPanelApp,
39
127
  onSelectSkill,
128
+ projectRoot,
40
129
  recentSkillValues,
41
130
  skillRecords,
42
131
  commands,
43
132
  } = params;
133
+ const [referenceMode, setReferenceMode] = useState<ContextReferenceMode>('root');
44
134
  const [inputSurfaceTrigger, setInputSurfaceTriggerState] = useState<ChatInputSurfaceTrigger | null>(null);
45
135
  const inputSurfaceTriggerSignatureRef = useRef('null');
46
136
  const setInputSurfaceTrigger = useCallback((nextTrigger: ChatInputSurfaceTrigger | null): void => {
47
137
  const nextSignature = JSON.stringify(nextTrigger ? [nextTrigger.key, nextTrigger.marker, nextTrigger.query, nextTrigger.start, nextTrigger.end] : null);
48
138
  if (inputSurfaceTriggerSignatureRef.current === nextSignature) return;
49
139
  inputSurfaceTriggerSignatureRef.current = nextSignature;
140
+ if (!nextTrigger) {
141
+ setReferenceMode('root');
142
+ }
50
143
  setInputSurfaceTriggerState(nextTrigger);
51
144
  }, []);
145
+ const isContextReferenceTrigger = inputSurfaceTrigger?.key === CONTEXT_REFERENCE_TRIGGER_SPEC.key;
146
+ const deferredReferenceQuery = useDeferredValue(
147
+ isContextReferenceTrigger ? inputSurfaceTrigger.query : '',
148
+ );
149
+ const shouldSearchServerPaths = Boolean(
150
+ isContextReferenceTrigger &&
151
+ projectRoot &&
152
+ (referenceMode === 'files' || deferredReferenceQuery.trim()),
153
+ );
154
+ const serverPathSearch = useServerPathSearch({
155
+ basePath: projectRoot,
156
+ query: deferredReferenceQuery,
157
+ enabled: shouldSearchServerPaths,
158
+ });
52
159
  const shouldLoadPanelApps =
53
- inputSurfaceTrigger?.key === PANEL_APP_REFERENCE_TRIGGER_SPEC.key ||
160
+ isContextReferenceTrigger ||
54
161
  inputSurfaceTrigger?.key === CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC.key;
55
162
  const panelApps = usePanelApps({ enabled: shouldLoadPanelApps });
56
163
 
57
164
  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
- ],
165
+ () => buildChatInputSurfacePlugins({
166
+ commands,
167
+ language,
168
+ onNavigate: setReferenceMode,
169
+ onSelectPanelApp,
170
+ onSelectSkill,
171
+ slashTexts: itemTexts.slashTexts,
172
+ }),
109
173
  [
110
174
  commands,
111
175
  itemTexts.slashTexts,
@@ -118,9 +182,16 @@ export function useChatInputSurfaceState(params: {
118
182
  () => resolveChatInputSurfaceState({
119
183
  data: {
120
184
  isPanelAppsLoading: panelApps.isLoading || panelApps.isFetching,
185
+ isServerPathSearchLoading: serverPathSearch.isLoading || serverPathSearch.isFetching,
121
186
  isSkillsLoading,
122
187
  panelApps: panelApps.data?.entries ?? [],
188
+ projectRoot,
123
189
  recentSkillValues,
190
+ referenceMode,
191
+ serverPathEntries: serverPathSearch.data?.entries ?? [],
192
+ serverPathSearchError: serverPathSearch.error instanceof Error
193
+ ? serverPathSearch.error.message
194
+ : null,
124
195
  skillRecords
125
196
  },
126
197
  plugins: inputSurfacePlugins,
@@ -132,6 +203,12 @@ export function useChatInputSurfaceState(params: {
132
203
  panelApps.data?.entries,
133
204
  panelApps.isFetching,
134
205
  panelApps.isLoading,
206
+ projectRoot,
207
+ referenceMode,
208
+ serverPathSearch.data?.entries,
209
+ serverPathSearch.error,
210
+ serverPathSearch.isFetching,
211
+ serverPathSearch.isLoading,
135
212
  isSkillsLoading,
136
213
  recentSkillValues,
137
214
  skillRecords
@@ -0,0 +1,175 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { resolveChatInputSurfaceState } from '@nextclaw/agent-chat-ui';
3
+ import {
4
+ CONTEXT_REFERENCE_TRIGGER_SPEC,
5
+ createContextReferenceInputSurfacePlugin,
6
+ } from '@/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils';
7
+ import type { PanelAppEntryView } from '@/shared/lib/api';
8
+
9
+ function createPanelApp(): PanelAppEntryView {
10
+ return {
11
+ id: 'task-board',
12
+ appId: 'task-board',
13
+ fileName: 'task-board.panel.html',
14
+ kind: 'single-file',
15
+ title: 'Task Board',
16
+ description: 'Track tasks',
17
+ contentPath: '/panels/task-board.panel.html',
18
+ createdAt: '2026-06-18T00:00:00.000Z',
19
+ updatedAt: '2026-06-18T00:00:00.000Z',
20
+ sizeBytes: 100,
21
+ favorite: false,
22
+ clientDeclared: false,
23
+ clientGranted: false,
24
+ openCount: 0,
25
+ };
26
+ }
27
+
28
+ function createPlugin(onNavigate = vi.fn()) {
29
+ return createContextReferenceInputSurfacePlugin({
30
+ itemTexts: {
31
+ context: {
32
+ backLabel: 'Back',
33
+ backDescription: 'Back to references',
34
+ backHintLabel: 'Enter to go back',
35
+ directoryDescription: 'Directory context',
36
+ fileDescription: 'File context',
37
+ filesDescription: 'Browse project files',
38
+ filesHintLabel: 'Enter to browse',
39
+ filesLabel: 'Files & Folders',
40
+ filesSubtitle: 'Project Context',
41
+ panelAppSectionLabel: 'Panel Apps',
42
+ projectRootLabel: 'Project directory',
43
+ searchFailedLabel: 'Search failed',
44
+ workspaceSectionLabel: 'Files & Folders',
45
+ },
46
+ panelApp: {
47
+ appIdLabel: 'App ID',
48
+ fileLabel: 'File',
49
+ noDescriptionLabel: 'No description',
50
+ subtitle: 'Panel App',
51
+ },
52
+ },
53
+ menuTexts: {
54
+ loadingLabel: 'Loading',
55
+ sectionLabel: 'References',
56
+ emptyLabel: 'Empty',
57
+ hintLabel: 'Type @',
58
+ itemHintLabel: 'Enter to reference',
59
+ },
60
+ onNavigate,
61
+ });
62
+ }
63
+
64
+ function createData(referenceMode: 'root' | 'files' = 'root') {
65
+ return {
66
+ isPanelAppsLoading: false,
67
+ isServerPathSearchLoading: false,
68
+ isSkillsLoading: false,
69
+ panelApps: [createPanelApp()],
70
+ projectRoot: '/tmp/project',
71
+ recentSkillValues: [],
72
+ referenceMode,
73
+ serverPathEntries: [
74
+ {
75
+ name: 'server-path.ts',
76
+ path: '/tmp/project/src/server-path.ts',
77
+ relativePath: 'src/server-path.ts',
78
+ parentRelativePath: 'src',
79
+ kind: 'file' as const,
80
+ hidden: false,
81
+ },
82
+ {
83
+ name: 'docs',
84
+ path: '/tmp/project/docs',
85
+ relativePath: 'docs',
86
+ parentRelativePath: '',
87
+ kind: 'directory' as const,
88
+ hidden: false,
89
+ },
90
+ ],
91
+ serverPathSearchError: null,
92
+ skillRecords: [],
93
+ };
94
+ }
95
+
96
+ describe('context reference input surface plugin', () => {
97
+ it('shows Files & Folders and panel apps in the root @ menu', () => {
98
+ const state = resolveChatInputSurfaceState({
99
+ plugins: [createPlugin()],
100
+ trigger: {
101
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
102
+ query: '',
103
+ start: 0,
104
+ end: 1,
105
+ },
106
+ data: createData(),
107
+ });
108
+
109
+ expect(state.panel?.items).toEqual([
110
+ expect.objectContaining({
111
+ title: 'Files & Folders',
112
+ icon: 'files',
113
+ selectionBehavior: 'navigate',
114
+ }),
115
+ expect.objectContaining({
116
+ key: 'panel-app:task-board',
117
+ tokenKind: 'panel_app',
118
+ tokenKey: 'task-board',
119
+ }),
120
+ ]);
121
+ });
122
+
123
+ it('builds file and directory tokens with path previews in files mode', () => {
124
+ const state = resolveChatInputSurfaceState({
125
+ plugins: [createPlugin()],
126
+ trigger: {
127
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
128
+ query: 'server',
129
+ start: 0,
130
+ end: 7,
131
+ },
132
+ data: createData('files'),
133
+ });
134
+
135
+ expect(state.panel?.items.slice(1)).toEqual([
136
+ expect.objectContaining({
137
+ title: 'server-path.ts',
138
+ tokenKind: 'workspace_file',
139
+ tokenKey: 'src/server-path.ts',
140
+ pathPreview: {
141
+ rootLabel: 'project',
142
+ segments: [
143
+ { label: 'src', kind: 'directory' },
144
+ { label: 'server-path.ts', kind: 'file' },
145
+ ],
146
+ },
147
+ }),
148
+ expect.objectContaining({
149
+ title: 'docs',
150
+ tokenKind: 'workspace_directory',
151
+ tokenKey: 'docs',
152
+ }),
153
+ ]);
154
+ });
155
+
156
+ it('navigates without producing a composer token', () => {
157
+ const onNavigate = vi.fn();
158
+ const state = resolveChatInputSurfaceState({
159
+ plugins: [createPlugin(onNavigate)],
160
+ trigger: {
161
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
162
+ query: '',
163
+ start: 0,
164
+ end: 1,
165
+ },
166
+ data: createData(),
167
+ });
168
+ const item = state.panel?.items[0];
169
+
170
+ state.panel?.onSelectItem?.(item!);
171
+
172
+ expect(onNavigate).toHaveBeenCalledWith('files');
173
+ expect(item).not.toHaveProperty('tokenKind');
174
+ });
175
+ });