@nextclaw/ui 0.15.19 → 0.15.21

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 (128) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/assets/api-CHo2gekJ.js +13 -0
  3. package/dist/assets/app-presenter-provider-2_DmWYT9.js +108 -0
  4. package/dist/assets/appearance-settings-page-BEy2UE7R.js +1 -0
  5. package/dist/assets/{book-open-HtUSPwoS.js → book-open-BrZL4o9L.js} +1 -1
  6. package/dist/assets/card-B5rRLu_6.js +1 -0
  7. package/dist/assets/{channels-list-page-Bspr1Jp4.js → channels-list-page-C1dnm68I.js} +2 -2
  8. package/dist/assets/chat-page-Dz6tvZ91.js +1 -0
  9. package/dist/assets/config-split-page-6y6W6dz4.js +1 -0
  10. package/dist/assets/confirm-dialog-q1CpEmR8.js +5 -0
  11. package/dist/assets/desktop-update-config-DrdlwP4j.js +1 -0
  12. package/dist/assets/{dist-CZx9Q6jc.js → dist-C9EX46wM.js} +1 -1
  13. package/dist/assets/doc-browser-BfttDSti.js +1 -0
  14. package/dist/assets/{doc-browser-B4l0Xu7u.js → doc-browser-DH-3hDnI.js} +1 -1
  15. package/dist/assets/{doc-browser-context-Dy7EhLI8.js → doc-browser-context-TU2heon0.js} +1 -1
  16. package/dist/assets/{eye-DU7p1-7X.js → eye-B-3dyZNA.js} +1 -1
  17. package/dist/assets/form-actions-DvvxCpL4.js +1 -0
  18. package/dist/assets/index-CXDPLDFD.css +1 -0
  19. package/dist/assets/index-k_eWScvN.js +2 -0
  20. package/dist/assets/loader-circle-B35NlHTI.js +1 -0
  21. package/dist/assets/logo-badge-DB51c0cS.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-CkPtwPBk.js +1 -0
  23. package/dist/assets/{mcp-marketplace-page-Dg03SZX1.js → mcp-marketplace-page-DWtsIqml.js} +3 -3
  24. package/dist/assets/{model-config-page-DOBqLZoQ.js → model-config-page-D6cGNnMM.js} +1 -1
  25. package/dist/assets/{navigation-link-Ct6xtnVM.js → navigation-link-BSJEUiwD.js} +1 -1
  26. package/dist/assets/plus-BRVe5jSJ.js +1 -0
  27. package/dist/assets/{popover-C2oIpGBF.js → popover-Bp9IpE_N.js} +1 -1
  28. package/dist/assets/provider-scoped-model-input-dk7fz9NL.js +1 -0
  29. package/dist/assets/{providers-config-page-BAVPJKpx.js → providers-config-page-DTP2YfxA.js} +1 -1
  30. package/dist/assets/react-D6ShNq95.js +8 -0
  31. package/dist/assets/{refresh-cw-BZ8giusN.js → refresh-cw-C2e94pmp.js} +1 -1
  32. package/dist/assets/remote-FllhXW3o.js +1 -0
  33. package/dist/assets/rotate-cw-CTC-Qszi.js +1 -0
  34. package/dist/assets/runtime-config-page-C57OLdPm.js +1 -0
  35. package/dist/assets/{save-CSzFGRse.js → save-CkwDOgvt.js} +1 -1
  36. package/dist/assets/{search-BmSWsiG2.js → search-CLXM2reO.js} +1 -1
  37. package/dist/assets/{search-config-page-CrL1pUzy.js → search-config-page-tnJXaaSr.js} +1 -1
  38. package/dist/assets/{secrets-config-page-BlgsD0-L.js → secrets-config-page-DQ_Curaj.js} +1 -1
  39. package/dist/assets/security-config-S00DjFfO.js +1 -0
  40. package/dist/assets/{select-CklWb6Rs.js → select-CNFBf00l.js} +1 -1
  41. package/dist/assets/{settings-2-BSZf8327.js → settings-2-B9IT2HGd.js} +1 -1
  42. package/dist/assets/skeleton-Dc06gatK.js +1 -0
  43. package/dist/assets/{tabs-Cah5klko.js → tabs-BuNr8LSO.js} +1 -1
  44. package/dist/assets/{tag-chip-DcrirDgN.js → tag-chip-CDkB5smU.js} +1 -1
  45. package/dist/assets/{trash-2-BLQOULWD.js → trash-2-GchaJuEM.js} +1 -1
  46. package/dist/assets/{use-config-CuFIsUVG.js → use-config-C69rR15i.js} +1 -1
  47. package/dist/assets/use-confirm-dialog-DF_EgPYx.js +1 -0
  48. package/dist/assets/{use-viewport-layout-DU29bUJS.js → use-viewport-layout-BACNZyMY.js} +1 -1
  49. package/dist/assets/x-BXFquY9e.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/app/components/__tests__/app-notification-runtime.test.tsx +32 -0
  53. package/src/app/components/app-notification-runtime.tsx +15 -0
  54. package/src/app/components/layout/sidebar-items.tsx +23 -9
  55. package/src/app/configs/app-navigation.config.ts +21 -0
  56. package/src/app/index.tsx +22 -2
  57. package/src/app/presenters/app.presenter.ts +8 -0
  58. package/src/features/chat/components/layout/chat-page-shell.tsx +7 -2
  59. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +12 -0
  60. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +28 -0
  61. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +4 -5
  62. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +13 -0
  63. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +31 -8
  64. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +79 -9
  65. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +16 -4
  66. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -2
  67. package/src/features/chat/features/input/input-surface-plugins/context-reference-items.utils.ts +240 -0
  68. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +62 -173
  69. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -0
  70. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  71. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -5
  72. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +12 -1
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +9 -5
  74. package/src/features/chat/index.ts +1 -0
  75. package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +177 -0
  76. package/src/features/chat/managers/chat-completion-notification.manager.ts +146 -0
  77. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +26 -0
  78. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +29 -2
  79. package/src/features/chat/pages/ncp-chat-page.tsx +9 -0
  80. package/src/features/inbox/components/inbox-delivery-content.test.tsx +57 -0
  81. package/src/features/inbox/components/inbox-delivery-content.tsx +118 -0
  82. package/src/features/inbox/components/inbox-reader-dialog.test.tsx +77 -0
  83. package/src/features/inbox/components/inbox-reader-dialog.tsx +224 -0
  84. package/src/features/inbox/components/inbox-runtime.tsx +14 -0
  85. package/src/features/inbox/hooks/use-inbox-deliveries.ts +14 -0
  86. package/src/features/inbox/index.ts +7 -0
  87. package/src/features/inbox/managers/inbox.manager.test.ts +86 -0
  88. package/src/features/inbox/managers/inbox.manager.ts +178 -0
  89. package/src/features/inbox/pages/inbox-page.test.ts +28 -0
  90. package/src/features/inbox/pages/inbox-page.tsx +385 -0
  91. package/src/features/inbox/stores/inbox.store.ts +23 -0
  92. package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +48 -0
  93. package/src/features/notifications/components/app-notification-toast.tsx +80 -0
  94. package/src/features/notifications/index.ts +8 -0
  95. package/src/features/notifications/managers/__tests__/app-notification.manager.test.ts +75 -0
  96. package/src/features/notifications/managers/app-notification.manager.ts +41 -0
  97. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +14 -7
  98. package/src/shared/hooks/use-projects.ts +2 -1
  99. package/src/shared/lib/i18n/index.ts +4 -0
  100. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -4
  101. package/src/shared/lib/i18n/locales/en-US/inbox.json +30 -0
  102. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -4
  103. package/src/shared/lib/i18n/locales/zh-CN/inbox.json +30 -0
  104. package/dist/assets/api-O5yEgzBt.js +0 -13
  105. package/dist/assets/app-presenter-provider-DXTYtH78.js +0 -106
  106. package/dist/assets/appearance-settings-page-BuGxSvlJ.js +0 -1
  107. package/dist/assets/card-GkBc-UMw.js +0 -1
  108. package/dist/assets/chat-page-BKQ89f4s.js +0 -1
  109. package/dist/assets/config-split-page-CvLXSFSC.js +0 -1
  110. package/dist/assets/confirm-dialog-BVH7haN0.js +0 -5
  111. package/dist/assets/desktop-update-config-C9ZUI8UW.js +0 -1
  112. package/dist/assets/doc-browser-DsVF1DrS.js +0 -1
  113. package/dist/assets/form-actions-aM9_jSJC.js +0 -1
  114. package/dist/assets/index-6pHX0DZH.js +0 -2
  115. package/dist/assets/index-DN6KM7WD.css +0 -1
  116. package/dist/assets/loader-circle-Dd6jw700.js +0 -1
  117. package/dist/assets/logo-badge-2-iV1pkr.js +0 -1
  118. package/dist/assets/mcp-marketplace-page-C-d0RMfg.js +0 -1
  119. package/dist/assets/plus-CsgeRId8.js +0 -1
  120. package/dist/assets/provider-scoped-model-input-DDqCdnTU.js +0 -1
  121. package/dist/assets/react-CeMr9V52.js +0 -8
  122. package/dist/assets/remote-C-r0SHcz.js +0 -1
  123. package/dist/assets/rotate-cw-DVdRfMSO.js +0 -1
  124. package/dist/assets/runtime-config-page-D0244uxJ.js +0 -1
  125. package/dist/assets/security-config-cDrdpMJW.js +0 -1
  126. package/dist/assets/skeleton-DG9u1BmA.js +0 -1
  127. package/dist/assets/use-confirm-dialog-CcySmkWp.js +0 -1
  128. package/dist/assets/x-BVoeXny1.js +0 -1
@@ -86,6 +86,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
86
86
  const state = resolveChatInputSurfaceState({
87
87
  data: {
88
88
  isPanelAppsLoading: false,
89
+ isProjectsLoading: false,
89
90
  isServerPathSearchLoading: false,
90
91
  isSkillsLoading: false,
91
92
  panelApps: [
@@ -96,8 +97,10 @@ describe('createSlashCommandInputSurfacePlugin', () => {
96
97
  }),
97
98
  ],
98
99
  projectRoot: '/tmp/project',
100
+ projects: [],
101
+ projectsError: null,
99
102
  recentSkillValues: [],
100
- referencePath: null,
103
+ referenceLocation: { view: 'root' },
101
104
  serverPathEntries: [],
102
105
  serverPathSearchError: null,
103
106
  skillRecords: [
@@ -153,12 +156,15 @@ describe('createSlashCommandInputSurfacePlugin', () => {
153
156
  const state = resolveChatInputSurfaceState({
154
157
  data: {
155
158
  isPanelAppsLoading: false,
159
+ isProjectsLoading: false,
156
160
  isServerPathSearchLoading: false,
157
161
  isSkillsLoading: false,
158
162
  panelApps: [],
159
163
  projectRoot: '/tmp/project',
164
+ projects: [],
165
+ projectsError: null,
160
166
  recentSkillValues: [],
161
- referencePath: null,
167
+ referenceLocation: { view: 'root' },
162
168
  serverPathEntries: [],
163
169
  serverPathSearchError: null,
164
170
  skillRecords: [
@@ -202,12 +208,15 @@ describe('createSlashCommandInputSurfacePlugin', () => {
202
208
  const state = resolveChatInputSurfaceState({
203
209
  data: {
204
210
  isPanelAppsLoading: false,
211
+ isProjectsLoading: false,
205
212
  isServerPathSearchLoading: false,
206
213
  isSkillsLoading: false,
207
214
  panelApps: [],
208
215
  projectRoot: '/tmp/project',
216
+ projects: [],
217
+ projectsError: null,
209
218
  recentSkillValues: [],
210
- referencePath: null,
219
+ referenceLocation: { view: 'root' },
211
220
  serverPathEntries: [],
212
221
  serverPathSearchError: null,
213
222
  skillRecords: [],
@@ -233,6 +242,7 @@ describe('createSlashCommandInputSurfacePlugin', () => {
233
242
  const state = resolveChatInputSurfaceState({
234
243
  data: {
235
244
  isPanelAppsLoading: false,
245
+ isProjectsLoading: false,
236
246
  isServerPathSearchLoading: false,
237
247
  isSkillsLoading: false,
238
248
  panelApps: [
@@ -243,8 +253,10 @@ describe('createSlashCommandInputSurfacePlugin', () => {
243
253
  }),
244
254
  ],
245
255
  projectRoot: '/tmp/project',
256
+ projects: [],
257
+ projectsError: null,
246
258
  recentSkillValues: [],
247
- referencePath: null,
259
+ referenceLocation: { view: 'root' },
248
260
  serverPathEntries: [],
249
261
  serverPathSearchError: null,
250
262
  skillRecords: [],
@@ -1,14 +1,22 @@
1
1
  import type { ChatSkillRecord } from '@/features/chat/types/chat-input-bar.types';
2
- import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
2
+ import type { PanelAppEntryView, ProjectView, ServerPathSearchEntryView } from '@/shared/lib/api';
3
+
4
+ export type ContextReferenceLocation =
5
+ | { view: 'root' }
6
+ | { view: 'files'; path: string }
7
+ | { view: 'projects' };
3
8
 
4
9
  export type ChatInputProductPluginData = {
5
10
  isPanelAppsLoading: boolean;
11
+ isProjectsLoading: boolean;
6
12
  isServerPathSearchLoading: boolean;
7
13
  isSkillsLoading: boolean;
8
14
  panelApps: readonly PanelAppEntryView[];
9
15
  projectRoot: string;
16
+ projects: readonly ProjectView[];
17
+ projectsError: string | null;
10
18
  recentSkillValues: readonly string[];
11
- referencePath: string | null;
19
+ referenceLocation: ContextReferenceLocation;
12
20
  serverPathEntries: readonly ServerPathSearchEntryView[];
13
21
  serverPathSearchError: string | null;
14
22
  skillRecords: readonly ChatSkillRecord[];
@@ -0,0 +1,240 @@
1
+ import type { ChatInputSurfaceItem } from '@nextclaw/agent-chat-ui';
2
+ import {
3
+ CHAT_PROJECT_TOKEN_KIND,
4
+ CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
5
+ CHAT_WORKSPACE_FILE_TOKEN_KIND,
6
+ } from '@nextclaw/shared';
7
+ import type { ProjectView, ServerPathSearchEntryView } from '@/shared/lib/api';
8
+ import type { ContextReferenceLocation } from './chat-input-product-plugin-adapters.types';
9
+ import {
10
+ resolveInputSurfaceMatchTier,
11
+ scoreInputSurfaceSearchCandidate,
12
+ } from './input-surface-search.utils';
13
+
14
+ export const FILES_NAVIGATION_ITEM_KEY = 'context-reference:navigate:files';
15
+ export const PROJECTS_NAVIGATION_ITEM_KEY = 'context-reference:navigate:projects';
16
+ export const ROOT_NAVIGATION_ITEM_KEY = 'context-reference:navigate:root';
17
+
18
+ const WORKSPACE_SECTION_KEY = 'workspace';
19
+ const PROJECT_SECTION_KEY = 'projects';
20
+
21
+ export type ContextReferenceInputSurfaceTexts = {
22
+ backLabel: string;
23
+ backDescription: string;
24
+ backHintLabel: string;
25
+ currentDirectoryLabel: string;
26
+ directoryDescription: string;
27
+ fileDescription: string;
28
+ filesDescription: string;
29
+ filesHintLabel: string;
30
+ filesLabel: string;
31
+ filesSubtitle: string;
32
+ panelAppSectionLabel: string;
33
+ parentLabel: string;
34
+ parentDescription: string;
35
+ parentHintLabel: string;
36
+ projectDescription: string;
37
+ projectPathLabel: string;
38
+ projectSectionLabel: string;
39
+ projectSubtitle: string;
40
+ projectsDescription: string;
41
+ projectsHintLabel: string;
42
+ projectsLabel: string;
43
+ projectsLoadFailedLabel: string;
44
+ projectsSubtitle: string;
45
+ projectRootLabel: string;
46
+ searchFailedLabel: string;
47
+ workspaceSectionLabel: string;
48
+ };
49
+
50
+ function resolveProjectLabel(projectRoot: string): string {
51
+ return projectRoot.split(/[\\/]+/).filter(Boolean).at(-1) ?? projectRoot;
52
+ }
53
+
54
+ function buildPathPreview(params: {
55
+ kind: 'directory' | 'file';
56
+ projectRoot: string;
57
+ relativePath: string;
58
+ }) {
59
+ const { kind, projectRoot, relativePath } = params;
60
+ const pathSegments = relativePath.split('/').filter(Boolean);
61
+ return {
62
+ rootLabel: resolveProjectLabel(projectRoot),
63
+ segments: pathSegments.map((label, index) => ({
64
+ label,
65
+ kind: index === pathSegments.length - 1 ? kind : 'directory' as const,
66
+ })),
67
+ };
68
+ }
69
+
70
+ export function buildProjectReferenceItems(params: {
71
+ projects: readonly ProjectView[];
72
+ query: string;
73
+ texts: ContextReferenceInputSurfaceTexts;
74
+ }): ChatInputSurfaceItem[] {
75
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
76
+ return params.projects
77
+ .map((project, order) => ({
78
+ order,
79
+ project,
80
+ score: scoreInputSurfaceSearchCandidate(
81
+ {
82
+ id: project.rootPath,
83
+ label: project.name,
84
+ description: project.rootPath,
85
+ aliases: project.template ? [project.template] : [],
86
+ },
87
+ params.query,
88
+ ),
89
+ }))
90
+ .filter(({ score }) => score > 0)
91
+ .sort((left, right) => {
92
+ const leftTier = resolveInputSurfaceMatchTier(left.score);
93
+ const rightTier = resolveInputSurfaceMatchTier(right.score);
94
+ if (rightTier !== leftTier) {
95
+ return rightTier - leftTier;
96
+ }
97
+ if (right.score !== left.score) {
98
+ return right.score - left.score;
99
+ }
100
+ return collator.compare(left.project.name, right.project.name) || left.order - right.order;
101
+ })
102
+ .map(({ project }) => ({
103
+ key: `context-reference:project:${project.rootPath}`,
104
+ icon: 'project',
105
+ title: project.name,
106
+ subtitle: params.texts.projectSubtitle,
107
+ description: params.texts.projectDescription,
108
+ detailLines: [`${params.texts.projectPathLabel}: ${project.rootPath}`],
109
+ sectionKey: PROJECT_SECTION_KEY,
110
+ sectionLabel: params.texts.projectSectionLabel,
111
+ value: project.rootPath,
112
+ tokenKind: CHAT_PROJECT_TOKEN_KIND,
113
+ tokenKey: project.rootPath,
114
+ }));
115
+ }
116
+
117
+ export function buildWorkspaceReferenceItems(params: {
118
+ entries: readonly ServerPathSearchEntryView[];
119
+ navigateDirectories: boolean;
120
+ projectRoot: string;
121
+ texts: ContextReferenceInputSurfaceTexts;
122
+ }): ChatInputSurfaceItem[] {
123
+ const projectLabel = resolveProjectLabel(params.projectRoot);
124
+ return params.entries.map((entry) => {
125
+ const navigates = params.navigateDirectories && entry.kind === 'directory';
126
+ return {
127
+ key: `workspace:${entry.kind}:${entry.relativePath}`,
128
+ icon: entry.kind === 'directory' ? 'folder' : 'file',
129
+ title: entry.name,
130
+ subtitle: entry.parentRelativePath || projectLabel,
131
+ description: entry.kind === 'directory'
132
+ ? params.texts.directoryDescription
133
+ : params.texts.fileDescription,
134
+ detailLines: [
135
+ `${params.texts.projectRootLabel}: ${params.projectRoot}`,
136
+ entry.relativePath,
137
+ ],
138
+ sectionKey: WORKSPACE_SECTION_KEY,
139
+ sectionLabel: params.texts.workspaceSectionLabel,
140
+ value: entry.relativePath,
141
+ tokenKind: navigates
142
+ ? undefined
143
+ : entry.kind === 'directory'
144
+ ? CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
145
+ : CHAT_WORKSPACE_FILE_TOKEN_KIND,
146
+ tokenKey: navigates ? undefined : entry.relativePath,
147
+ selectionBehavior: navigates ? 'navigate' : undefined,
148
+ pathPreview: buildPathPreview({
149
+ kind: entry.kind,
150
+ projectRoot: params.projectRoot,
151
+ relativePath: entry.relativePath,
152
+ }),
153
+ } satisfies ChatInputSurfaceItem;
154
+ });
155
+ }
156
+
157
+ export function buildCurrentDirectoryReferenceItem(params: {
158
+ projectRoot: string;
159
+ referencePath: string;
160
+ texts: ContextReferenceInputSurfaceTexts;
161
+ }): ChatInputSurfaceItem {
162
+ const { projectRoot, referencePath, texts } = params;
163
+ const title = referencePath.split('/').filter(Boolean).at(-1) ?? referencePath;
164
+ return {
165
+ key: `context-reference:current-directory:${referencePath}`,
166
+ icon: 'folder',
167
+ title: texts.currentDirectoryLabel,
168
+ subtitle: title,
169
+ description: texts.directoryDescription,
170
+ detailLines: [
171
+ `${texts.projectRootLabel}: ${projectRoot}`,
172
+ referencePath,
173
+ ],
174
+ sectionKey: WORKSPACE_SECTION_KEY,
175
+ sectionLabel: texts.workspaceSectionLabel,
176
+ value: referencePath,
177
+ tokenKind: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
178
+ tokenKey: referencePath,
179
+ pathPreview: buildPathPreview({
180
+ kind: 'directory',
181
+ projectRoot,
182
+ relativePath: referencePath,
183
+ }),
184
+ };
185
+ }
186
+
187
+ export function buildFilesNavigationItem(
188
+ texts: ContextReferenceInputSurfaceTexts,
189
+ ): ChatInputSurfaceItem {
190
+ return {
191
+ key: FILES_NAVIGATION_ITEM_KEY,
192
+ icon: 'files',
193
+ title: texts.filesLabel,
194
+ subtitle: texts.filesSubtitle,
195
+ description: texts.filesDescription,
196
+ detailLines: [],
197
+ hintLabel: texts.filesHintLabel,
198
+ selectionBehavior: 'navigate',
199
+ };
200
+ }
201
+
202
+ export function buildProjectsNavigationItem(
203
+ texts: ContextReferenceInputSurfaceTexts,
204
+ ): ChatInputSurfaceItem {
205
+ return {
206
+ key: PROJECTS_NAVIGATION_ITEM_KEY,
207
+ icon: 'project',
208
+ title: texts.projectsLabel,
209
+ subtitle: texts.projectsSubtitle,
210
+ description: texts.projectsDescription,
211
+ detailLines: [],
212
+ hintLabel: texts.projectsHintLabel,
213
+ selectionBehavior: 'navigate',
214
+ };
215
+ }
216
+
217
+ function resolveParentReferencePath(referencePath: string): string | null {
218
+ const segments = referencePath.split('/').filter(Boolean);
219
+ return segments.length === 0 ? null : segments.slice(0, -1).join('/');
220
+ }
221
+
222
+ export function buildBackNavigationItem(params: {
223
+ location: ContextReferenceLocation;
224
+ texts: ContextReferenceInputSurfaceTexts;
225
+ }): ChatInputSurfaceItem {
226
+ const { location, texts } = params;
227
+ const referencePath = location.view === 'files' ? location.path : '';
228
+ const isRootView = location.view !== 'files' || !referencePath;
229
+ return {
230
+ key: ROOT_NAVIGATION_ITEM_KEY,
231
+ icon: 'back',
232
+ title: isRootView ? texts.backLabel : texts.parentLabel,
233
+ subtitle: '',
234
+ description: isRootView ? texts.backDescription : texts.parentDescription,
235
+ detailLines: [],
236
+ hintLabel: isRootView ? texts.backHintLabel : texts.parentHintLabel,
237
+ value: resolveParentReferencePath(referencePath) ?? '',
238
+ selectionBehavior: 'navigate',
239
+ };
240
+ }
@@ -5,20 +5,28 @@ import {
5
5
  type ChatInputSurfacePlugin,
6
6
  type ChatInputSurfaceTriggerSpec,
7
7
  } from '@nextclaw/agent-chat-ui';
8
+ import type { PanelAppEntryView } from '@/shared/lib/api';
9
+ import type {
10
+ ChatInputProductPluginData,
11
+ ContextReferenceLocation,
12
+ } from './chat-input-product-plugin-adapters.types';
8
13
  import {
9
- CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
10
- CHAT_WORKSPACE_FILE_TOKEN_KIND,
11
- } from '@nextclaw/shared';
12
- import type { PanelAppEntryView, ServerPathSearchEntryView } from '@/shared/lib/api';
13
- import type { ChatInputProductPluginData } from './chat-input-product-plugin-adapters.types';
14
+ buildBackNavigationItem,
15
+ buildCurrentDirectoryReferenceItem,
16
+ buildFilesNavigationItem,
17
+ buildProjectReferenceItems,
18
+ buildProjectsNavigationItem,
19
+ buildWorkspaceReferenceItems,
20
+ FILES_NAVIGATION_ITEM_KEY,
21
+ PROJECTS_NAVIGATION_ITEM_KEY,
22
+ ROOT_NAVIGATION_ITEM_KEY,
23
+ type ContextReferenceInputSurfaceTexts,
24
+ } from './context-reference-items.utils';
14
25
  import {
15
26
  scoreInputSurfaceSearchCandidate,
16
27
  resolveInputSurfaceMatchTier,
17
28
  } from './input-surface-search.utils';
18
29
 
19
- const FILES_NAVIGATION_ITEM_KEY = 'context-reference:navigate:files';
20
- const ROOT_NAVIGATION_ITEM_KEY = 'context-reference:navigate:root';
21
- const WORKSPACE_SECTION_KEY = 'workspace';
22
30
  const PANEL_APP_SECTION_KEY = 'panel-apps';
23
31
 
24
32
  export const CONTEXT_REFERENCE_TRIGGER_SPEC: ChatInputSurfaceTriggerSpec = {
@@ -33,34 +41,10 @@ export type PanelAppInputSurfaceItemTexts = {
33
41
  subtitle: string;
34
42
  };
35
43
 
36
- export type ContextReferenceInputSurfaceTexts = {
37
- backLabel: string;
38
- backDescription: string;
39
- backHintLabel: string;
40
- currentDirectoryLabel: string;
41
- directoryDescription: string;
42
- fileDescription: string;
43
- filesDescription: string;
44
- filesHintLabel: string;
45
- filesLabel: string;
46
- filesSubtitle: string;
47
- panelAppSectionLabel: string;
48
- parentLabel: string;
49
- parentDescription: string;
50
- parentHintLabel: string;
51
- projectRootLabel: string;
52
- searchFailedLabel: string;
53
- workspaceSectionLabel: string;
54
- };
55
-
56
44
  function getPanelAppActivityTime(entry: PanelAppEntryView): number {
57
45
  return Date.parse(entry.lastOpenedAt ?? entry.updatedAt) || 0;
58
46
  }
59
47
 
60
- function resolveProjectLabel(projectRoot: string): string {
61
- return projectRoot.split(/[\\/]+/).filter(Boolean).at(-1) ?? projectRoot;
62
- }
63
-
64
48
  function resolvePanelAppInputSurfaceEntries(params: {
65
49
  entries: readonly PanelAppEntryView[];
66
50
  query: string;
@@ -155,171 +139,64 @@ function buildPanelAppReferenceItems(params: {
155
139
  }));
156
140
  }
157
141
 
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
-
174
- function buildWorkspaceReferenceItems(params: {
175
- entries: readonly ServerPathSearchEntryView[];
176
- navigateDirectories: boolean;
177
- projectRoot: string;
178
- texts: ContextReferenceInputSurfaceTexts;
179
- }): ChatInputSurfaceItem[] {
180
- const projectLabel = resolveProjectLabel(params.projectRoot);
181
- return params.entries.map((entry) => {
182
- const navigates = params.navigateDirectories && entry.kind === 'directory';
183
- return {
184
- key: `workspace:${entry.kind}:${entry.relativePath}`,
185
- icon: entry.kind === 'directory' ? 'folder' : 'file',
186
- title: entry.name,
187
- subtitle: entry.parentRelativePath || projectLabel,
188
- description: entry.kind === 'directory'
189
- ? params.texts.directoryDescription
190
- : params.texts.fileDescription,
191
- detailLines: [
192
- `${params.texts.projectRootLabel}: ${params.projectRoot}`,
193
- entry.relativePath,
194
- ],
195
- sectionKey: WORKSPACE_SECTION_KEY,
196
- sectionLabel: params.texts.workspaceSectionLabel,
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
- }),
210
- } satisfies ChatInputSurfaceItem;
211
- });
212
- }
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
-
252
- function buildFilesNavigationItem(texts: ContextReferenceInputSurfaceTexts): ChatInputSurfaceItem {
253
- return {
254
- key: FILES_NAVIGATION_ITEM_KEY,
255
- icon: 'files',
256
- title: texts.filesLabel,
257
- subtitle: texts.filesSubtitle,
258
- description: texts.filesDescription,
259
- detailLines: [],
260
- hintLabel: texts.filesHintLabel,
261
- selectionBehavior: 'navigate',
262
- };
263
- }
264
-
265
- function buildBackNavigationItem(params: {
266
- referencePath: string;
267
- texts: ContextReferenceInputSurfaceTexts;
268
- }): ChatInputSurfaceItem {
269
- const { referencePath, texts } = params;
270
- const isProjectRoot = !referencePath;
271
- return {
272
- key: ROOT_NAVIGATION_ITEM_KEY,
273
- icon: 'back',
274
- title: isProjectRoot ? texts.backLabel : texts.parentLabel,
275
- subtitle: '',
276
- description: isProjectRoot ? texts.backDescription : texts.parentDescription,
277
- detailLines: [],
278
- hintLabel: isProjectRoot ? texts.backHintLabel : texts.parentHintLabel,
279
- value: resolveParentReferencePath(referencePath) ?? '',
280
- selectionBehavior: 'navigate',
281
- };
282
- }
283
-
284
142
  export function createContextReferenceInputSurfacePlugin(params: {
285
143
  itemTexts: {
286
144
  context: ContextReferenceInputSurfaceTexts;
287
145
  panelApp: PanelAppInputSurfaceItemTexts;
288
146
  };
289
147
  menuTexts: ChatInputSurfaceMenuTexts;
290
- onNavigate: (path: string | null) => void;
148
+ onNavigate: (location: ContextReferenceLocation) => void;
291
149
  }): ChatInputSurfacePlugin<ChatInputProductPluginData> {
292
150
  return createInputSurfaceTriggeredPanelPlugin({
293
151
  key: 'context-reference',
294
152
  trigger: CONTEXT_REFERENCE_TRIGGER_SPEC,
295
153
  resolvePanel: ({ data, trigger }) => {
296
- const isFilesMode = data.referencePath !== null;
154
+ const isFilesMode = data.referenceLocation.view === 'files';
155
+ const isProjectsMode = data.referenceLocation.view === 'projects';
297
156
  const isBrowsing = isFilesMode && !trigger.query.trim();
157
+ const referencePath = data.referenceLocation.view === 'files'
158
+ ? data.referenceLocation.path
159
+ : '';
298
160
  const workspaceItems = buildWorkspaceReferenceItems({
299
161
  entries: data.serverPathEntries,
300
162
  navigateDirectories: isBrowsing,
301
163
  projectRoot: data.projectRoot,
302
164
  texts: params.itemTexts.context,
303
165
  });
304
- const currentDirectoryItem = isBrowsing && data.referencePath
166
+ const projectItems = buildProjectReferenceItems({
167
+ projects: data.projects,
168
+ query: trigger.query,
169
+ texts: params.itemTexts.context,
170
+ });
171
+ const currentDirectoryItem = isBrowsing && referencePath
305
172
  ? buildCurrentDirectoryReferenceItem({
306
173
  projectRoot: data.projectRoot,
307
- referencePath: data.referencePath,
174
+ referencePath,
308
175
  texts: params.itemTexts.context,
309
176
  })
310
177
  : null;
311
178
  const items = isFilesMode
312
179
  ? [
313
180
  buildBackNavigationItem({
314
- referencePath: data.referencePath ?? '',
181
+ location: data.referenceLocation,
315
182
  texts: params.itemTexts.context,
316
183
  }),
317
184
  ...(currentDirectoryItem ? [currentDirectoryItem] : []),
318
185
  ...workspaceItems,
319
186
  ]
320
- : [
187
+ : isProjectsMode
188
+ ? [
189
+ buildBackNavigationItem({
190
+ location: data.referenceLocation,
191
+ texts: params.itemTexts.context,
192
+ }),
193
+ ...projectItems,
194
+ ]
195
+ : [
321
196
  buildFilesNavigationItem(params.itemTexts.context),
197
+ buildProjectsNavigationItem(params.itemTexts.context),
322
198
  ...(trigger.query ? workspaceItems : []),
199
+ ...(trigger.query ? projectItems : []),
323
200
  ...buildPanelAppReferenceItems({
324
201
  entries: data.panelApps,
325
202
  query: trigger.query,
@@ -329,25 +206,37 @@ export function createContextReferenceInputSurfacePlugin(params: {
329
206
  ];
330
207
  const relevantLoading = isFilesMode
331
208
  ? data.isServerPathSearchLoading
332
- : data.isPanelAppsLoading || (Boolean(trigger.query) && data.isServerPathSearchLoading);
209
+ : isProjectsMode
210
+ ? data.isProjectsLoading
211
+ : data.isPanelAppsLoading ||
212
+ (Boolean(trigger.query) && (data.isProjectsLoading || data.isServerPathSearchLoading));
213
+ const errorMessage = isProjectsMode && data.projectsError
214
+ ? `${params.itemTexts.context.projectsLoadFailedLabel}: ${data.projectsError}`
215
+ : data.serverPathSearchError
216
+ ? `${params.itemTexts.context.searchFailedLabel}: ${data.serverPathSearchError}`
217
+ : null;
333
218
  return {
334
- isLoading: isFilesMode
335
- ? relevantLoading
336
- : relevantLoading && items.length === 0,
219
+ isLoading: isProjectsMode ? relevantLoading : relevantLoading && items.length === 0,
337
220
  items,
338
- notice: data.serverPathSearchError
221
+ notice: errorMessage
339
222
  ? {
340
- message: `${params.itemTexts.context.searchFailedLabel}: ${data.serverPathSearchError}`,
223
+ message: errorMessage,
341
224
  tone: 'error',
342
225
  }
343
226
  : undefined,
344
227
  onSelectItem: (item) => {
345
228
  if (item.key === FILES_NAVIGATION_ITEM_KEY) {
346
- params.onNavigate('');
229
+ params.onNavigate({ view: 'files', path: '' });
230
+ } else if (item.key === PROJECTS_NAVIGATION_ITEM_KEY) {
231
+ params.onNavigate({ view: 'projects' });
347
232
  } else if (item.key === ROOT_NAVIGATION_ITEM_KEY) {
348
- params.onNavigate(data.referencePath ? item.value ?? '' : null);
233
+ params.onNavigate(
234
+ isFilesMode && referencePath
235
+ ? { view: 'files', path: item.value ?? '' }
236
+ : { view: 'root' },
237
+ );
349
238
  } else if (item.selectionBehavior === 'navigate' && item.value) {
350
- params.onNavigate(item.value);
239
+ params.onNavigate({ view: 'files', path: item.value });
351
240
  }
352
241
  },
353
242
  texts: params.menuTexts,
@@ -226,6 +226,35 @@ describe('chat inline token workspace references', () => {
226
226
  ]);
227
227
  });
228
228
 
229
+ it('serializes project references with their registered path and display name', () => {
230
+ expect(
231
+ buildInlineTokensFromComposer([
232
+ createChatComposerTokenNode({
233
+ tokenKind: 'project',
234
+ tokenKey: '/tmp/NextClaw Project',
235
+ label: 'NextClaw',
236
+ }),
237
+ ]),
238
+ ).toEqual([
239
+ {
240
+ kind: 'project',
241
+ key: '/tmp/NextClaw Project',
242
+ label: 'NextClaw',
243
+ rawText: '@project:%2Ftmp%2FNextClaw%20Project',
244
+ },
245
+ ]);
246
+ expect(
247
+ buildInlineTokensFromTextProtocol('review @project:%2Ftmp%2FNextClaw%20Project'),
248
+ ).toEqual([
249
+ {
250
+ kind: 'project',
251
+ key: '/tmp/NextClaw Project',
252
+ label: 'NextClaw Project',
253
+ rawText: '@project:%2Ftmp%2FNextClaw%20Project',
254
+ },
255
+ ]);
256
+ });
257
+
229
258
  it('resolves workspace token paths inside POSIX and Windows project roots', () => {
230
259
  expect(resolveWorkspaceReferencePath({
231
260
  projectRoot: '/tmp/project/',