@nextclaw/ui 0.15.6 → 0.15.8

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 (106) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/assets/{api-Dyb5rgh0.js → api-kGIZo6kv.js} +1 -1
  3. package/dist/assets/{app-presenter-provider-7jV0nPiL.js → app-presenter-provider-Ct0DePOC.js} +48 -48
  4. package/dist/assets/{appearance-settings-page-DFm3BbEb.js → appearance-settings-page-CcA8Kewq.js} +1 -1
  5. package/dist/assets/{book-open-L6LCuqOr.js → book-open-D4LvR1N9.js} +1 -1
  6. package/dist/assets/{channels-list-page-DOaPKNJA.js → channels-list-page-CybW_0nu.js} +1 -1
  7. package/dist/assets/{chat-page-Cw9uDhD_.js → chat-page-Bs8c1adx.js} +1 -1
  8. package/dist/assets/{config-split-page-CqlqjnCK.js → config-split-page-BvhZDJGS.js} +1 -1
  9. package/dist/assets/{confirm-dialog-bAp8NU93.js → confirm-dialog-Ucx_SQm4.js} +1 -1
  10. package/dist/assets/desktop-update-config-Ch9m1GRB.js +1 -0
  11. package/dist/assets/{dist-CrtbGLlh.js → dist-CCwmPmQ3.js} +1 -1
  12. package/dist/assets/doc-browser-Cr7330LU2.js +1 -0
  13. package/dist/assets/{doc-browser-BmnQa30-.js → doc-browser-N9UwkWvd.js} +1 -1
  14. package/dist/assets/{doc-browser-context-BlpUUFrR.js → doc-browser-context-CIIKT2sE.js} +1 -1
  15. package/dist/assets/{download-BhZZNXD8.js → download-CSAHAjF9.js} +1 -1
  16. package/dist/assets/{external-link-B9Uoq5J7.js → external-link-Dno-cJJr.js} +1 -1
  17. package/dist/assets/{eye-K5knauEM.js → eye-BZkraQUn.js} +1 -1
  18. package/dist/assets/{folder-BIYZZWJv.js → folder-Bro2sQD3.js} +1 -1
  19. package/dist/assets/{form-actions-CKprWFeM.js → form-actions-CC31CR28.js} +1 -1
  20. package/dist/assets/index-BlKbckX4.css +1 -0
  21. package/dist/assets/index-EO5Gsaff.js +2 -0
  22. package/dist/assets/loader-circle-DyToFBCM.js +1 -0
  23. package/dist/assets/{logo-badge-BQ-XFZod.js → logo-badge-B___Witw.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-3Q-VOIJ3.js +1 -0
  25. package/dist/assets/{mcp-marketplace-page-BfA7A3n4.js → mcp-marketplace-page-Ccy-UG8R.js} +1 -1
  26. package/dist/assets/{model-config-page-BbWbW_qM.js → model-config-page-BsMxhVaO.js} +1 -1
  27. package/dist/assets/plus-wynE6QCr.js +1 -0
  28. package/dist/assets/{popover-CX9zFsuc.js → popover-CAy9XSdY.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-B-9_DtIP.js → provider-scoped-model-input-Cqo78Dx_.js} +1 -1
  30. package/dist/assets/{providers-config-page-xwIjGur2.js → providers-config-page-CZSc-ruy.js} +1 -1
  31. package/dist/assets/react-UuyIZ_Oi.js +8 -0
  32. package/dist/assets/{refresh-cw-kXdvbhFf.js → refresh-cw-w9L_vlM-.js} +1 -1
  33. package/dist/assets/remote-D4rv7kb-.js +1 -0
  34. package/dist/assets/rotate-cw-C0VFtLju.js +1 -0
  35. package/dist/assets/{runtime-config-page-DTsi7czJ.js → runtime-config-page-DM63Nkxm.js} +1 -1
  36. package/dist/assets/{save-C9qpz2_2.js → save-DdL5VkJS.js} +1 -1
  37. package/dist/assets/{search-_kEi29qo.js → search-DydusQAS.js} +1 -1
  38. package/dist/assets/{search-config-page-H7ug8f4D.js → search-config-page-CfwWop50.js} +1 -1
  39. package/dist/assets/{secrets-config-page-C1_6rINu.js → secrets-config-page-BPIZUwLW.js} +1 -1
  40. package/dist/assets/security-config-CeBjHoFL.js +1 -0
  41. package/dist/assets/{select-MJKhKAWR.js → select-BEtjsC3L.js} +1 -1
  42. package/dist/assets/{setting-row-DA3tJuew.js → setting-row-CTA16hhx.js} +1 -1
  43. package/dist/assets/{settings-2-D_y7zXJz.js → settings-2-D6BrZlD5.js} +1 -1
  44. package/dist/assets/{skeleton-0uTLUhm9.js → skeleton-DcJKUZCH.js} +1 -1
  45. package/dist/assets/{tabs-h44XPocd.js → tabs-CrPVCYHg.js} +1 -1
  46. package/dist/assets/{tag-chip-bRZ04q3C.js → tag-chip-Bvm-V7SR.js} +1 -1
  47. package/dist/assets/{trash-2-BRpSE-Wt.js → trash-2-5ABWNTB-.js} +1 -1
  48. package/dist/assets/{use-config-CwDEAl6f.js → use-config-DJaZYyZC.js} +1 -1
  49. package/dist/assets/{use-confirm-dialog-BbnWw-8c.js → use-confirm-dialog-QUWL1EuB.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-CqgIMDQL.js → use-viewport-layout-BWYz8zwd.js} +1 -1
  51. package/dist/assets/x-gM1Q7cRX.js +1 -0
  52. package/dist/index.html +30 -29
  53. package/index.html +1 -0
  54. package/package.json +6 -6
  55. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +13 -2
  56. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +18 -0
  57. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +47 -2
  58. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +63 -16
  59. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +54 -4
  60. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +4 -4
  61. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +1 -3
  62. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +110 -27
  63. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +17 -1
  64. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +17 -0
  65. package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
  66. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
  67. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
  68. package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
  69. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  70. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +55 -0
  71. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
  72. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
  73. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
  74. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
  75. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
  76. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
  77. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
  78. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  79. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
  80. package/src/features/marketplace/components/marketplace-page.tsx +10 -2
  81. package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
  82. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
  83. package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
  84. package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
  85. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +21 -0
  86. package/src/features/system-status/components/desktop-update-config.tsx +8 -5
  87. package/src/shared/components/common/__tests__/brand-header.test.tsx +39 -17
  88. package/src/shared/components/common/brand-header.tsx +55 -51
  89. package/src/shared/lib/i18n/locales/en-US/chat.json +5 -0
  90. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
  91. package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
  92. package/src/shared/lib/i18n/locales/zh-CN/chat.json +5 -0
  93. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
  94. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
  95. package/dist/assets/desktop-update-config-2SjMfCMl.js +0 -1
  96. package/dist/assets/doc-browser-A5IRyDXQ2.js +0 -1
  97. package/dist/assets/index-BVn702m4.css +0 -1
  98. package/dist/assets/index-CFxJ1Fkg.js +0 -2
  99. package/dist/assets/loader-circle-Bj2RMrCR.js +0 -1
  100. package/dist/assets/mcp-marketplace-page-CXHYO95j.js +0 -1
  101. package/dist/assets/plus-TmFdwO3P.js +0 -1
  102. package/dist/assets/react-DsgudwpT.js +0 -8
  103. package/dist/assets/remote-B9F2uCk9.js +0 -1
  104. package/dist/assets/rotate-cw-DEc6aihr.js +0 -1
  105. package/dist/assets/security-config-Cr2ERKLu.js +0 -1
  106. package/dist/assets/x-DdEDL6ju.js +0 -1
@@ -54,6 +54,24 @@ vi.mock('@/features/chat/features/input/hooks/use-chat-model-favorites', () => (
54
54
  }),
55
55
  }));
56
56
 
57
+ vi.mock('@/shared/hooks/use-server-path-browse', () => ({
58
+ useServerPathBrowse: () => ({
59
+ data: { entries: [] },
60
+ error: null,
61
+ isFetching: false,
62
+ isLoading: false,
63
+ }),
64
+ }));
65
+
66
+ vi.mock('@/shared/hooks/use-server-path-search', () => ({
67
+ useServerPathSearch: () => ({
68
+ data: { entries: [] },
69
+ error: null,
70
+ isFetching: false,
71
+ isLoading: false,
72
+ }),
73
+ }));
74
+
57
75
  vi.mock('@/shared/lib/api', async (importOriginal) => {
58
76
  const actual = await importOriginal();
59
77
  return {
@@ -17,6 +17,23 @@ const useServerPathSearchMock = vi.hoisted(() =>
17
17
  isLoading: false,
18
18
  })),
19
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
+ );
20
37
 
21
38
  vi.mock('@/features/panel-apps', () => ({
22
39
  usePanelApps: usePanelAppsMock,
@@ -24,6 +41,9 @@ vi.mock('@/features/panel-apps', () => ({
24
41
  vi.mock('@/shared/hooks/use-server-path-search', () => ({
25
42
  useServerPathSearch: useServerPathSearchMock,
26
43
  }));
44
+ vi.mock('@/shared/hooks/use-server-path-browse', () => ({
45
+ useServerPathBrowse: useServerPathBrowseMock,
46
+ }));
27
47
 
28
48
  function createHookParams() {
29
49
  return {
@@ -48,10 +68,11 @@ function createHookParams() {
48
68
 
49
69
  beforeEach(() => {
50
70
  usePanelAppsMock.mockClear();
71
+ useServerPathBrowseMock.mockClear();
51
72
  useServerPathSearchMock.mockClear();
52
73
  });
53
74
 
54
- it('enables project path search after navigating into files and folders', () => {
75
+ it('browses nested folders while reserving search for project-wide queries', () => {
55
76
  const { result } = renderHook(() => useChatInputSurfaceState(createHookParams()));
56
77
 
57
78
  act(() =>
@@ -68,9 +89,33 @@ it('enables project path search after navigating into files and folders', () =>
68
89
  );
69
90
  act(() => result.current.inputSurfaceState.panel?.onSelectItem?.(filesItem!));
70
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
+ }));
71
116
  expect(useServerPathSearchMock).toHaveBeenLastCalledWith({
72
117
  basePath: '/tmp/project',
73
- query: '',
118
+ query: 'needle',
74
119
  enabled: true,
75
120
  });
76
121
  });
@@ -10,13 +10,14 @@ import {
10
10
  CONTEXT_REFERENCE_TRIGGER_SPEC,
11
11
  createContextReferenceInputSurfacePlugin,
12
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';
14
13
  import {
15
14
  createSlashCommandInputSurfacePlugin,
16
15
  type ChatSlashCommandDescriptor,
17
16
  } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
18
17
  import type { ChatInputBarAdapterTexts, ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
18
+ import { useServerPathBrowse } from '@/shared/hooks/use-server-path-browse';
19
19
  import { useServerPathSearch } from '@/shared/hooks/use-server-path-search';
20
+ import type { ServerPathEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
20
21
 
21
22
  type ChatInputSurfaceSlashTexts = Pick<
22
23
  ChatInputBarAdapterTexts,
@@ -38,7 +39,7 @@ type UseChatInputSurfaceStateParams = {
38
39
  function buildChatInputSurfacePlugins(params: {
39
40
  commands: readonly ChatSlashCommandDescriptor[];
40
41
  language: I18nLanguage;
41
- onNavigate: (mode: ContextReferenceMode) => void;
42
+ onNavigate: (path: string | null) => void;
42
43
  onSelectPanelApp: (appId: string) => void;
43
44
  onSelectSkill: (skillRef: string) => void;
44
45
  slashTexts: ChatInputSurfaceSlashTexts;
@@ -93,6 +94,7 @@ function buildChatInputSurfacePlugins(params: {
93
94
  backLabel: t('chatContextReferenceBack', language),
94
95
  backDescription: t('chatContextReferenceBackDescription', language),
95
96
  backHintLabel: t('chatContextReferenceBackHint', language),
97
+ currentDirectoryLabel: t('chatContextReferenceCurrentDirectory', language),
96
98
  directoryDescription: t('chatContextReferenceDirectoryDescription', language),
97
99
  fileDescription: t('chatContextReferenceFileDescription', language),
98
100
  filesDescription: t('chatContextReferenceFilesDescription', language),
@@ -100,6 +102,9 @@ function buildChatInputSurfacePlugins(params: {
100
102
  filesLabel: t('chatContextReferenceFilesLabel', language),
101
103
  filesSubtitle: t('chatContextReferenceFilesType', language),
102
104
  panelAppSectionLabel: t('chatPanelAppReferenceSection', language),
105
+ parentLabel: t('chatContextReferenceParent', language),
106
+ parentDescription: t('chatContextReferenceParentDescription', language),
107
+ parentHintLabel: t('chatContextReferenceParentHint', language),
103
108
  projectRootLabel: t('chatContextReferenceProjectRoot', language),
104
109
  searchFailedLabel: t('chatContextReferenceSearchFailed', language),
105
110
  workspaceSectionLabel: t('chatContextReferenceWorkspaceSection', language),
@@ -118,6 +123,21 @@ function buildChatInputSurfacePlugins(params: {
118
123
  ];
119
124
  }
120
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
+
121
141
  export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams) {
122
142
  const {
123
143
  isSkillsLoading,
@@ -130,7 +150,7 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
130
150
  skillRecords,
131
151
  commands,
132
152
  } = params;
133
- const [referenceMode, setReferenceMode] = useState<ContextReferenceMode>('root');
153
+ const [referencePath, setReferencePath] = useState<string | null>(null);
134
154
  const [inputSurfaceTrigger, setInputSurfaceTriggerState] = useState<ChatInputSurfaceTrigger | null>(null);
135
155
  const inputSurfaceTriggerSignatureRef = useRef('null');
136
156
  const setInputSurfaceTrigger = useCallback((nextTrigger: ChatInputSurfaceTrigger | null): void => {
@@ -138,7 +158,7 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
138
158
  if (inputSurfaceTriggerSignatureRef.current === nextSignature) return;
139
159
  inputSurfaceTriggerSignatureRef.current = nextSignature;
140
160
  if (!nextTrigger) {
141
- setReferenceMode('root');
161
+ setReferencePath(null);
142
162
  }
143
163
  setInputSurfaceTriggerState(nextTrigger);
144
164
  }, []);
@@ -146,16 +166,43 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
146
166
  const deferredReferenceQuery = useDeferredValue(
147
167
  isContextReferenceTrigger ? inputSurfaceTrigger.query : '',
148
168
  );
169
+ const isBrowsingServerPaths = Boolean(
170
+ isContextReferenceTrigger &&
171
+ projectRoot &&
172
+ referencePath !== null &&
173
+ !deferredReferenceQuery.trim(),
174
+ );
149
175
  const shouldSearchServerPaths = Boolean(
150
176
  isContextReferenceTrigger &&
151
177
  projectRoot &&
152
- (referenceMode === 'files' || deferredReferenceQuery.trim()),
178
+ deferredReferenceQuery.trim(),
153
179
  );
180
+ const serverPathBrowse = useServerPathBrowse({
181
+ path: referencePath || '.',
182
+ basePath: projectRoot,
183
+ includeFiles: true,
184
+ enabled: isBrowsingServerPaths,
185
+ });
154
186
  const serverPathSearch = useServerPathSearch({
155
187
  basePath: projectRoot,
156
188
  query: deferredReferenceQuery,
157
189
  enabled: shouldSearchServerPaths,
158
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
+ );
159
206
  const shouldLoadPanelApps =
160
207
  isContextReferenceTrigger ||
161
208
  inputSurfaceTrigger?.key === CHAT_INPUT_SURFACE_SLASH_TRIGGER_SPEC.key;
@@ -165,7 +212,7 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
165
212
  () => buildChatInputSurfacePlugins({
166
213
  commands,
167
214
  language,
168
- onNavigate: setReferenceMode,
215
+ onNavigate: setReferencePath,
169
216
  onSelectPanelApp,
170
217
  onSelectSkill,
171
218
  slashTexts: itemTexts.slashTexts,
@@ -182,15 +229,15 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
182
229
  () => resolveChatInputSurfaceState({
183
230
  data: {
184
231
  isPanelAppsLoading: panelApps.isLoading || panelApps.isFetching,
185
- isServerPathSearchLoading: serverPathSearch.isLoading || serverPathSearch.isFetching,
232
+ isServerPathSearchLoading: activeServerPathQuery.isLoading || activeServerPathQuery.isFetching,
186
233
  isSkillsLoading,
187
234
  panelApps: panelApps.data?.entries ?? [],
188
235
  projectRoot,
189
236
  recentSkillValues,
190
- referenceMode,
191
- serverPathEntries: serverPathSearch.data?.entries ?? [],
192
- serverPathSearchError: serverPathSearch.error instanceof Error
193
- ? serverPathSearch.error.message
237
+ referencePath,
238
+ serverPathEntries,
239
+ serverPathSearchError: activeServerPathQuery.error instanceof Error
240
+ ? activeServerPathQuery.error.message
194
241
  : null,
195
242
  skillRecords
196
243
  },
@@ -204,11 +251,11 @@ export function useChatInputSurfaceState(params: UseChatInputSurfaceStateParams)
204
251
  panelApps.isFetching,
205
252
  panelApps.isLoading,
206
253
  projectRoot,
207
- referenceMode,
208
- serverPathSearch.data?.entries,
209
- serverPathSearch.error,
210
- serverPathSearch.isFetching,
211
- serverPathSearch.isLoading,
254
+ activeServerPathQuery.error,
255
+ activeServerPathQuery.isFetching,
256
+ activeServerPathQuery.isLoading,
257
+ referencePath,
258
+ serverPathEntries,
212
259
  isSkillsLoading,
213
260
  recentSkillValues,
214
261
  skillRecords
@@ -32,6 +32,7 @@ function createPlugin(onNavigate = vi.fn()) {
32
32
  backLabel: 'Back',
33
33
  backDescription: 'Back to references',
34
34
  backHintLabel: 'Enter to go back',
35
+ currentDirectoryLabel: 'Reference current folder',
35
36
  directoryDescription: 'Directory context',
36
37
  fileDescription: 'File context',
37
38
  filesDescription: 'Browse project files',
@@ -39,6 +40,9 @@ function createPlugin(onNavigate = vi.fn()) {
39
40
  filesLabel: 'Files & Folders',
40
41
  filesSubtitle: 'Project Context',
41
42
  panelAppSectionLabel: 'Panel Apps',
43
+ parentLabel: 'Up one folder',
44
+ parentDescription: 'Browse parent',
45
+ parentHintLabel: 'Enter to browse parent',
42
46
  projectRootLabel: 'Project directory',
43
47
  searchFailedLabel: 'Search failed',
44
48
  workspaceSectionLabel: 'Files & Folders',
@@ -61,7 +65,7 @@ function createPlugin(onNavigate = vi.fn()) {
61
65
  });
62
66
  }
63
67
 
64
- function createData(referenceMode: 'root' | 'files' = 'root') {
68
+ function createData(referencePath: string | null = null) {
65
69
  return {
66
70
  isPanelAppsLoading: false,
67
71
  isServerPathSearchLoading: false,
@@ -69,7 +73,7 @@ function createData(referenceMode: 'root' | 'files' = 'root') {
69
73
  panelApps: [createPanelApp()],
70
74
  projectRoot: '/tmp/project',
71
75
  recentSkillValues: [],
72
- referenceMode,
76
+ referencePath,
73
77
  serverPathEntries: [
74
78
  {
75
79
  name: 'server-path.ts',
@@ -129,7 +133,7 @@ describe('context reference input surface plugin', () => {
129
133
  start: 0,
130
134
  end: 7,
131
135
  },
132
- data: createData('files'),
136
+ data: createData(''),
133
137
  });
134
138
 
135
139
  expect(state.panel?.items.slice(1)).toEqual([
@@ -169,7 +173,53 @@ describe('context reference input surface plugin', () => {
169
173
 
170
174
  state.panel?.onSelectItem?.(item!);
171
175
 
172
- expect(onNavigate).toHaveBeenCalledWith('files');
176
+ expect(onNavigate).toHaveBeenCalledWith('');
173
177
  expect(item).not.toHaveProperty('tokenKind');
174
178
  });
179
+
180
+ it('opens browsed directories and offers the current folder as a token', () => {
181
+ const onNavigate = vi.fn();
182
+ const rootState = resolveChatInputSurfaceState({
183
+ plugins: [createPlugin(onNavigate)],
184
+ trigger: {
185
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
186
+ query: '',
187
+ start: 0,
188
+ end: 1,
189
+ },
190
+ data: createData(''),
191
+ });
192
+ const directoryItem = rootState.panel?.items.find((item) => item.title === 'docs');
193
+
194
+ expect(directoryItem).toMatchObject({
195
+ selectionBehavior: 'navigate',
196
+ value: 'docs',
197
+ });
198
+ expect(directoryItem?.tokenKind).toBeUndefined();
199
+ rootState.panel?.onSelectItem?.(directoryItem!);
200
+ expect(onNavigate).toHaveBeenCalledWith('docs');
201
+
202
+ const nestedState = resolveChatInputSurfaceState({
203
+ plugins: [createPlugin(onNavigate)],
204
+ trigger: {
205
+ ...CONTEXT_REFERENCE_TRIGGER_SPEC,
206
+ query: '',
207
+ start: 0,
208
+ end: 1,
209
+ },
210
+ data: createData('docs'),
211
+ });
212
+ expect(nestedState.panel?.items).toEqual(expect.arrayContaining([
213
+ expect.objectContaining({
214
+ title: 'Reference current folder',
215
+ subtitle: 'docs',
216
+ tokenKind: 'workspace_directory',
217
+ tokenKey: 'docs',
218
+ }),
219
+ expect.objectContaining({
220
+ title: 'Up one folder',
221
+ selectionBehavior: 'navigate',
222
+ }),
223
+ ]));
224
+ });
175
225
  });
@@ -97,7 +97,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
97
97
  ],
98
98
  projectRoot: '/tmp/project',
99
99
  recentSkillValues: [],
100
- referenceMode: 'root',
100
+ referencePath: null,
101
101
  serverPathEntries: [],
102
102
  serverPathSearchError: null,
103
103
  skillRecords: [
@@ -158,7 +158,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
158
158
  panelApps: [],
159
159
  projectRoot: '/tmp/project',
160
160
  recentSkillValues: [],
161
- referenceMode: 'root',
161
+ referencePath: null,
162
162
  serverPathEntries: [],
163
163
  serverPathSearchError: null,
164
164
  skillRecords: [
@@ -207,7 +207,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
207
207
  panelApps: [],
208
208
  projectRoot: '/tmp/project',
209
209
  recentSkillValues: [],
210
- referenceMode: 'root',
210
+ referencePath: null,
211
211
  serverPathEntries: [],
212
212
  serverPathSearchError: null,
213
213
  skillRecords: [],
@@ -244,7 +244,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
244
244
  ],
245
245
  projectRoot: '/tmp/project',
246
246
  recentSkillValues: [],
247
- referenceMode: 'root',
247
+ referencePath: null,
248
248
  serverPathEntries: [],
249
249
  serverPathSearchError: null,
250
250
  skillRecords: [],
@@ -1,8 +1,6 @@
1
1
  import type { ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
2
2
  import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
3
3
 
4
- export type ContextReferenceMode = 'root' | 'files';
5
-
6
4
  export type ChatInputProductPluginData = {
7
5
  isPanelAppsLoading: boolean;
8
6
  isServerPathSearchLoading: boolean;
@@ -10,7 +8,7 @@ export type ChatInputProductPluginData = {
10
8
  panelApps: readonly PanelAppEntryView[];
11
9
  projectRoot: string;
12
10
  recentSkillValues: readonly string[];
13
- referenceMode: ContextReferenceMode;
11
+ referencePath: string | null;
14
12
  serverPathEntries: readonly ServerPathSearchEntryView[];
15
13
  serverPathSearchError: string | null;
16
14
  skillRecords: readonly ChatSkillRecord[];
@@ -10,10 +10,7 @@ import {
10
10
  CHAT_WORKSPACE_FILE_TOKEN_KIND,
11
11
  } from '@nextclaw/shared';
12
12
  import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
13
- import type {
14
- ChatInputProductPluginData,
15
- ContextReferenceMode,
16
- } from './chat-input-product-plugin-adapters.types';
13
+ import type { ChatInputProductPluginData } from './chat-input-product-plugin-adapters.types';
17
14
  import {
18
15
  scoreInputSurfaceSearchCandidate,
19
16
  resolveInputSurfaceMatchTier,
@@ -40,6 +37,7 @@ export type ContextReferenceInputSurfaceTexts = {
40
37
  backLabel: string;
41
38
  backDescription: string;
42
39
  backHintLabel: string;
40
+ currentDirectoryLabel: string;
43
41
  directoryDescription: string;
44
42
  fileDescription: string;
45
43
  filesDescription: string;
@@ -47,6 +45,9 @@ export type ContextReferenceInputSurfaceTexts = {
47
45
  filesLabel: string;
48
46
  filesSubtitle: string;
49
47
  panelAppSectionLabel: string;
48
+ parentLabel: string;
49
+ parentDescription: string;
50
+ parentHintLabel: string;
50
51
  projectRootLabel: string;
51
52
  searchFailedLabel: string;
52
53
  workspaceSectionLabel: string;
@@ -154,14 +155,31 @@ function buildPanelAppReferenceItems(params: {
154
155
  }));
155
156
  }
156
157
 
158
+ function buildPathPreview(params: {
159
+ kind: 'directory' | 'file';
160
+ projectRoot: string;
161
+ relativePath: string;
162
+ }) {
163
+ const { kind, projectRoot, relativePath } = params;
164
+ const pathSegments = relativePath.split('/').filter(Boolean);
165
+ return {
166
+ rootLabel: resolveProjectLabel(projectRoot),
167
+ segments: pathSegments.map((label, index) => ({
168
+ label,
169
+ kind: index === pathSegments.length - 1 ? kind : 'directory' as const,
170
+ })),
171
+ };
172
+ }
173
+
157
174
  function buildWorkspaceReferenceItems(params: {
158
175
  entries: readonly ServerPathSearchEntryView[];
176
+ navigateDirectories: boolean;
159
177
  projectRoot: string;
160
178
  texts: ContextReferenceInputSurfaceTexts;
161
179
  }): ChatInputSurfaceItem[] {
162
180
  const projectLabel = resolveProjectLabel(params.projectRoot);
163
181
  return params.entries.map((entry) => {
164
- const pathSegments = entry.relativePath.split('/').filter(Boolean);
182
+ const navigates = params.navigateDirectories && entry.kind === 'directory';
165
183
  return {
166
184
  key: `workspace:${entry.kind}:${entry.relativePath}`,
167
185
  icon: entry.kind === 'directory' ? 'folder' : 'file',
@@ -176,21 +194,61 @@ function buildWorkspaceReferenceItems(params: {
176
194
  ],
177
195
  sectionKey: WORKSPACE_SECTION_KEY,
178
196
  sectionLabel: params.texts.workspaceSectionLabel,
179
- tokenKind: entry.kind === 'directory'
180
- ? CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
181
- : CHAT_WORKSPACE_FILE_TOKEN_KIND,
182
- tokenKey: entry.relativePath,
183
- pathPreview: {
184
- rootLabel: projectLabel,
185
- segments: pathSegments.map((label, index) => ({
186
- label,
187
- kind: index === pathSegments.length - 1 ? entry.kind : 'directory',
188
- })),
189
- },
197
+ value: entry.relativePath,
198
+ tokenKind: navigates
199
+ ? undefined
200
+ : entry.kind === 'directory'
201
+ ? CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
202
+ : CHAT_WORKSPACE_FILE_TOKEN_KIND,
203
+ tokenKey: navigates ? undefined : entry.relativePath,
204
+ selectionBehavior: navigates ? 'navigate' : undefined,
205
+ pathPreview: buildPathPreview({
206
+ kind: entry.kind,
207
+ projectRoot: params.projectRoot,
208
+ relativePath: entry.relativePath,
209
+ }),
190
210
  } satisfies ChatInputSurfaceItem;
191
211
  });
192
212
  }
193
213
 
214
+ function resolveParentReferencePath(referencePath: string): string | null {
215
+ const segments = referencePath.split('/').filter(Boolean);
216
+ if (segments.length === 0) {
217
+ return null;
218
+ }
219
+ return segments.slice(0, -1).join('/');
220
+ }
221
+
222
+ function buildCurrentDirectoryReferenceItem(params: {
223
+ projectRoot: string;
224
+ referencePath: string;
225
+ texts: ContextReferenceInputSurfaceTexts;
226
+ }): ChatInputSurfaceItem {
227
+ const { projectRoot, referencePath, texts } = params;
228
+ const title = referencePath.split('/').filter(Boolean).at(-1) ?? referencePath;
229
+ return {
230
+ key: `context-reference:current-directory:${referencePath}`,
231
+ icon: 'folder',
232
+ title: texts.currentDirectoryLabel,
233
+ subtitle: title,
234
+ description: texts.directoryDescription,
235
+ detailLines: [
236
+ `${texts.projectRootLabel}: ${projectRoot}`,
237
+ referencePath,
238
+ ],
239
+ sectionKey: WORKSPACE_SECTION_KEY,
240
+ sectionLabel: texts.workspaceSectionLabel,
241
+ value: referencePath,
242
+ tokenKind: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
243
+ tokenKey: referencePath,
244
+ pathPreview: buildPathPreview({
245
+ kind: 'directory',
246
+ projectRoot,
247
+ relativePath: referencePath,
248
+ }),
249
+ };
250
+ }
251
+
194
252
  function buildFilesNavigationItem(texts: ContextReferenceInputSurfaceTexts): ChatInputSurfaceItem {
195
253
  return {
196
254
  key: FILES_NAVIGATION_ITEM_KEY,
@@ -204,15 +262,21 @@ function buildFilesNavigationItem(texts: ContextReferenceInputSurfaceTexts): Cha
204
262
  };
205
263
  }
206
264
 
207
- function buildRootNavigationItem(texts: ContextReferenceInputSurfaceTexts): ChatInputSurfaceItem {
265
+ function buildBackNavigationItem(params: {
266
+ referencePath: string;
267
+ texts: ContextReferenceInputSurfaceTexts;
268
+ }): ChatInputSurfaceItem {
269
+ const { referencePath, texts } = params;
270
+ const isProjectRoot = !referencePath;
208
271
  return {
209
272
  key: ROOT_NAVIGATION_ITEM_KEY,
210
273
  icon: 'back',
211
- title: texts.backLabel,
274
+ title: isProjectRoot ? texts.backLabel : texts.parentLabel,
212
275
  subtitle: '',
213
- description: texts.backDescription,
276
+ description: isProjectRoot ? texts.backDescription : texts.parentDescription,
214
277
  detailLines: [],
215
- hintLabel: texts.backHintLabel,
278
+ hintLabel: isProjectRoot ? texts.backHintLabel : texts.parentHintLabel,
279
+ value: resolveParentReferencePath(referencePath) ?? '',
216
280
  selectionBehavior: 'navigate',
217
281
  };
218
282
  }
@@ -223,19 +287,36 @@ export function createContextReferenceInputSurfacePlugin(params: {
223
287
  panelApp: PanelAppInputSurfaceItemTexts;
224
288
  };
225
289
  menuTexts: ChatInputSurfaceMenuTexts;
226
- onNavigate: (mode: ContextReferenceMode) => void;
290
+ onNavigate: (path: string | null) => void;
227
291
  }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
228
292
  return createInputSurfaceTriggeredPanelPlugin({
229
293
  key: 'context-reference',
230
294
  trigger: CONTEXT_REFERENCE_TRIGGER_SPEC,
231
295
  resolvePanel: ({ data, trigger }) => {
296
+ const isFilesMode = data.referencePath !== null;
297
+ const isBrowsing = isFilesMode && !trigger.query.trim();
232
298
  const workspaceItems = buildWorkspaceReferenceItems({
233
299
  entries: data.serverPathEntries,
300
+ navigateDirectories: isBrowsing,
234
301
  projectRoot: data.projectRoot,
235
302
  texts: params.itemTexts.context,
236
303
  });
237
- const items = data.referenceMode === 'files'
238
- ? [buildRootNavigationItem(params.itemTexts.context), ...workspaceItems]
304
+ const currentDirectoryItem = isBrowsing && data.referencePath
305
+ ? buildCurrentDirectoryReferenceItem({
306
+ projectRoot: data.projectRoot,
307
+ referencePath: data.referencePath,
308
+ texts: params.itemTexts.context,
309
+ })
310
+ : null;
311
+ const items = isFilesMode
312
+ ? [
313
+ buildBackNavigationItem({
314
+ referencePath: data.referencePath ?? '',
315
+ texts: params.itemTexts.context,
316
+ }),
317
+ ...(currentDirectoryItem ? [currentDirectoryItem] : []),
318
+ ...workspaceItems,
319
+ ]
239
320
  : [
240
321
  buildFilesNavigationItem(params.itemTexts.context),
241
322
  ...(trigger.query ? workspaceItems : []),
@@ -246,11 +327,11 @@ export function createContextReferenceInputSurfacePlugin(params: {
246
327
  texts: params.itemTexts.panelApp,
247
328
  }),
248
329
  ];
249
- const relevantLoading = data.referenceMode === 'files'
330
+ const relevantLoading = isFilesMode
250
331
  ? data.isServerPathSearchLoading
251
332
  : data.isPanelAppsLoading || (Boolean(trigger.query) && data.isServerPathSearchLoading);
252
333
  return {
253
- isLoading: data.referenceMode === 'files'
334
+ isLoading: isFilesMode
254
335
  ? relevantLoading
255
336
  : relevantLoading && items.length === 0,
256
337
  items,
@@ -262,9 +343,11 @@ export function createContextReferenceInputSurfacePlugin(params: {
262
343
  : undefined,
263
344
  onSelectItem: (item) => {
264
345
  if (item.key === FILES_NAVIGATION_ITEM_KEY) {
265
- params.onNavigate('files');
346
+ params.onNavigate('');
266
347
  } else if (item.key === ROOT_NAVIGATION_ITEM_KEY) {
267
- params.onNavigate('root');
348
+ params.onNavigate(data.referencePath ? item.value ?? '' : null);
349
+ } else if (item.selectionBehavior === 'navigate' && item.value) {
350
+ params.onNavigate(item.value);
268
351
  }
269
352
  },
270
353
  texts: params.menuTexts,