@nextclaw/ui 0.15.18 → 0.15.20

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 (114) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/api-tWUa6l9J.js +13 -0
  3. package/dist/assets/app-presenter-provider-s-FrOinZ.js +106 -0
  4. package/dist/assets/{appearance-settings-page-BkQaPtQ-.js → appearance-settings-page-dMRBD7ni.js} +1 -1
  5. package/dist/assets/{book-open-WsEnuW9P.js → book-open-BQ29ZmDE.js} +1 -1
  6. package/dist/assets/{card-xVrS1b1T.js → card-CtKueklV.js} +1 -1
  7. package/dist/assets/{channels-list-page-CAWuTHSM.js → channels-list-page-C8y3rqYt.js} +2 -2
  8. package/dist/assets/chat-page-MXBltS3G.js +1 -0
  9. package/dist/assets/{config-split-page-xj0UtMow.js → config-split-page-uC6d_r9c.js} +1 -1
  10. package/dist/assets/{confirm-dialog-e2YCUzt1.js → confirm-dialog-C1pbqHZX.js} +1 -1
  11. package/dist/assets/desktop-update-config-DssP8LCk.js +1 -0
  12. package/dist/assets/{dist-Ca-jiFrK.js → dist-rYHnBo8t.js} +1 -1
  13. package/dist/assets/doc-browser-AXF1zRsM.js +1 -0
  14. package/dist/assets/doc-browser-D8ng4EUc.js +1 -0
  15. package/dist/assets/doc-browser-context-CPHNzQ8m.js +1 -0
  16. package/dist/assets/{eye-B30J471A.js → eye-CiJz3ODY.js} +1 -1
  17. package/dist/assets/{form-actions-CnSzu3WD.js → form-actions-BnZVzLZf.js} +1 -1
  18. package/dist/assets/index-CocHWEsI.js +2 -0
  19. package/dist/assets/index-DN6KM7WD.css +1 -0
  20. package/dist/assets/loader-circle-AOpK5kkW.js +1 -0
  21. package/dist/assets/{logo-badge-CISsltAo.js → logo-badge-BXI-Kdhd.js} +1 -1
  22. package/dist/assets/mcp-marketplace-page-5ogeBHnl.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-CxNe-Q9Q.js +9 -0
  24. package/dist/assets/model-config-page-CR_mgEVp.js +1 -0
  25. package/dist/assets/{navigation-link-qPPUiLt0.js → navigation-link-C1cvB7y4.js} +1 -1
  26. package/dist/assets/plus-DI37QuHc.js +1 -0
  27. package/dist/assets/{popover-DHx55dbc.js → popover-ZoDwVwwd.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-1TJxAhx6.js → provider-scoped-model-input-DIL6Ex_d.js} +1 -1
  29. package/dist/assets/{providers-config-page-BwHtHIPe.js → providers-config-page-D2NXVctW.js} +1 -1
  30. package/dist/assets/react-39CDNZlM.js +8 -0
  31. package/dist/assets/{refresh-cw-C7bynrWX.js → refresh-cw-DKewcdAl.js} +1 -1
  32. package/dist/assets/remote-DYOOO1YG.js +1 -0
  33. package/dist/assets/rotate-cw-PmqA8LQT.js +1 -0
  34. package/dist/assets/{runtime-config-page-BLrTVO3n.js → runtime-config-page-0lb5NyKc.js} +1 -1
  35. package/dist/assets/{save-CsRsFtHS.js → save-BrckuAXY.js} +1 -1
  36. package/dist/assets/{search-D0HtJIL-.js → search-BhXQB2TO.js} +1 -1
  37. package/dist/assets/{search-config-page-n1GElgij.js → search-config-page-DFOQ6SDJ.js} +1 -1
  38. package/dist/assets/{secrets-config-page-DbUc3Ggi.js → secrets-config-page-BqqeClqE.js} +2 -2
  39. package/dist/assets/security-config-13AtPIlm.js +1 -0
  40. package/dist/assets/{select-C1zfp9zM.js → select-CVzGGT38.js} +1 -1
  41. package/dist/assets/{settings-2-Ba3cLSN2.js → settings-2-BP6L2enG.js} +1 -1
  42. package/dist/assets/skeleton-OwCNQJ7M.js +1 -0
  43. package/dist/assets/{tabs-DmKgMKzb.js → tabs-B-QWjKCL.js} +1 -1
  44. package/dist/assets/{tag-chip-2vwlVUwM.js → tag-chip-C88mxjwx.js} +1 -1
  45. package/dist/assets/{trash-2-CUpWjNHu.js → trash-2-ck3t33I3.js} +1 -1
  46. package/dist/assets/use-config-BAr5bH2c.js +1 -0
  47. package/dist/assets/{use-confirm-dialog-CeUEE4p5.js → use-confirm-dialog-C4e0nG_4.js} +1 -1
  48. package/dist/assets/{use-viewport-layout-DaKJ-kap.js → use-viewport-layout-k1isXfNm.js} +1 -1
  49. package/dist/assets/x-C1ijNWcE.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +28 -0
  53. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +4 -5
  54. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +13 -0
  55. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +31 -8
  56. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +79 -9
  57. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +16 -4
  58. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -2
  59. package/src/features/chat/features/input/input-surface-plugins/context-reference-items.utils.ts +240 -0
  60. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +62 -173
  61. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -0
  62. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  63. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -5
  64. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +35 -0
  65. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +12 -1
  66. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +3 -1
  67. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +13 -5
  68. package/src/features/chat/features/message/components/chat-message-list.container.tsx +10 -5
  69. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +9 -0
  70. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +4 -1
  71. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +29 -0
  72. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  73. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +32 -14
  74. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +41 -0
  75. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +4 -0
  76. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +15 -2
  77. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +11 -0
  78. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +1 -0
  79. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +11 -8
  80. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +6 -0
  81. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  82. package/src/features/chat/managers/chat-ui.manager.ts +10 -4
  83. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +26 -0
  84. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  85. package/src/features/chat/stores/chat-thread.store.ts +6 -1
  86. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +32 -0
  87. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +2 -0
  88. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +20 -38
  89. package/src/shared/components/doc-browser/types/doc-browser.types.ts +5 -0
  90. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +63 -0
  91. package/src/shared/hooks/use-projects.ts +2 -1
  92. package/src/shared/lib/i18n/locales/en-US/chat.json +14 -4
  93. package/src/shared/lib/i18n/locales/zh-CN/chat.json +14 -4
  94. package/dist/assets/api-D5UCLVE_.js +0 -13
  95. package/dist/assets/app-presenter-provider-De7Zwitu.js +0 -106
  96. package/dist/assets/chat-page-AvOuaxuv.js +0 -1
  97. package/dist/assets/desktop-update-config-D5NIaimo.js +0 -1
  98. package/dist/assets/doc-browser-B2tNEKrJ.js +0 -1
  99. package/dist/assets/doc-browser-CZX6I6CU.js +0 -1
  100. package/dist/assets/doc-browser-context-CGz0LULR.js +0 -1
  101. package/dist/assets/index-B5AoUrMf.js +0 -2
  102. package/dist/assets/index-QveCjxCj.css +0 -1
  103. package/dist/assets/loader-circle-DjwSRQHW.js +0 -1
  104. package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +0 -1
  105. package/dist/assets/mcp-marketplace-page-CPcPzMUQ.js +0 -9
  106. package/dist/assets/model-config-page-D6ex3_NM.js +0 -1
  107. package/dist/assets/plus-C45awBb1.js +0 -1
  108. package/dist/assets/react-DerwD2EA.js +0 -8
  109. package/dist/assets/remote-B6TXClIg.js +0 -1
  110. package/dist/assets/rotate-cw-Bhx0XkQP.js +0 -1
  111. package/dist/assets/security-config-n2WHhHXV.js +0 -1
  112. package/dist/assets/skeleton-DL5npsVL.js +0 -1
  113. package/dist/assets/use-config-BvxDWV-M.js +0 -1
  114. package/dist/assets/x-DGdYT7rH.js +0 -1
@@ -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/',
@@ -1,10 +1,12 @@
1
1
  import {
2
+ CHAT_PROJECT_TOKEN_KIND,
2
3
  CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
3
4
  CHAT_WORKSPACE_FILE_TOKEN_KIND,
4
5
  } from '@nextclaw/shared';
5
6
 
6
7
  const CHAT_SKILL_TOKEN_PREFIX = '$';
7
8
  const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
9
+ const CHAT_PROJECT_TOKEN_PREFIX = '@project:';
8
10
  const CHAT_WORKSPACE_FILE_TOKEN_PREFIX = '@file:';
9
11
  const CHAT_WORKSPACE_DIRECTORY_TOKEN_PREFIX = '@folder:';
10
12
 
@@ -20,6 +22,9 @@ export function serializeChatComposerTokenText(params: {
20
22
  if (tokenKind === 'panel_app') {
21
23
  return `${CHAT_PANEL_APP_TOKEN_PREFIX}${tokenKey}`;
22
24
  }
25
+ if (tokenKind === CHAT_PROJECT_TOKEN_KIND) {
26
+ return `${CHAT_PROJECT_TOKEN_PREFIX}${encodeURIComponent(tokenKey)}`;
27
+ }
23
28
  if (tokenKind === CHAT_WORKSPACE_FILE_TOKEN_KIND) {
24
29
  return `${CHAT_WORKSPACE_FILE_TOKEN_PREFIX}${encodeURIComponent(tokenKey)}`;
25
30
  }
@@ -2,6 +2,7 @@ import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
2
2
  import {
3
3
  CHAT_INLINE_TOKENS_METADATA_KEY,
4
4
  CHAT_INLINE_TOKENS_SCHEMA_VERSION,
5
+ CHAT_PROJECT_TOKEN_KIND,
5
6
  CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
6
7
  CHAT_WORKSPACE_FILE_TOKEN_KIND,
7
8
  type ChatInlineTokenMetadata,
@@ -13,6 +14,7 @@ import { serializeChatComposerTokenText } from './chat-composer-token-protocol.u
13
14
  export { CHAT_INLINE_TOKENS_METADATA_KEY };
14
15
  const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
15
16
  const CHAT_PANEL_APP_TOKEN_PATTERN = /@panel-app:([A-Za-z0-9_-]+)/g;
17
+ const CHAT_PROJECT_TOKEN_PATTERN = /@project:([^\s]+)/g;
16
18
  const CHAT_WORKSPACE_FILE_TOKEN_PATTERN = /@file:([^\s]+)/g;
17
19
  const CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN = /@folder:([^\s]+)/g;
18
20
 
@@ -96,6 +98,7 @@ export function buildInlineTokensFromComposer(
96
98
  }
97
99
  if (
98
100
  node.tokenKind !== 'skill' &&
101
+ node.tokenKind !== CHAT_PROJECT_TOKEN_KIND &&
99
102
  node.tokenKind !== CHAT_WORKSPACE_FILE_TOKEN_KIND &&
100
103
  node.tokenKind !== CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND
101
104
  ) {
@@ -121,6 +124,15 @@ export function buildInlineTokensFromComposer(
121
124
  });
122
125
  continue;
123
126
  }
127
+ if (node.tokenKind === CHAT_PROJECT_TOKEN_KIND) {
128
+ tokens.push({
129
+ kind: CHAT_PROJECT_TOKEN_KIND,
130
+ key: node.tokenKey,
131
+ label: node.label,
132
+ rawText,
133
+ });
134
+ continue;
135
+ }
124
136
  const workspaceKind = node.tokenKind === CHAT_WORKSPACE_FILE_TOKEN_KIND
125
137
  ? CHAT_WORKSPACE_FILE_TOKEN_KIND
126
138
  : CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND;
@@ -134,8 +146,11 @@ export function buildInlineTokensFromComposer(
134
146
  return dedupeInlineTokens(tokens);
135
147
  }
136
148
 
137
- function appendWorkspaceTokens(params: {
138
- kind: typeof CHAT_WORKSPACE_FILE_TOKEN_KIND | typeof CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND;
149
+ function appendEncodedKeyTokens(params: {
150
+ kind:
151
+ | typeof CHAT_PROJECT_TOKEN_KIND
152
+ | typeof CHAT_WORKSPACE_FILE_TOKEN_KIND
153
+ | typeof CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND;
139
154
  pattern: RegExp;
140
155
  text: string;
141
156
  tokens: ChatInlineTokenSource[];
@@ -155,7 +170,7 @@ function appendWorkspaceTokens(params: {
155
170
  tokens.push({
156
171
  kind,
157
172
  key,
158
- label: key.split('/').filter(Boolean).at(-1) ?? key,
173
+ label: key.split(/[\\/]+/).filter(Boolean).at(-1) ?? key,
159
174
  rawText: match[0],
160
175
  });
161
176
  }
@@ -175,13 +190,19 @@ export function buildInlineTokensFromTextProtocol(text: string): ChatInlineToken
175
190
  rawText: `${CHAT_PANEL_APP_TOKEN_PREFIX}${key}`
176
191
  });
177
192
  }
178
- appendWorkspaceTokens({
193
+ appendEncodedKeyTokens({
194
+ kind: CHAT_PROJECT_TOKEN_KIND,
195
+ pattern: CHAT_PROJECT_TOKEN_PATTERN,
196
+ text,
197
+ tokens,
198
+ });
199
+ appendEncodedKeyTokens({
179
200
  kind: CHAT_WORKSPACE_FILE_TOKEN_KIND,
180
201
  pattern: CHAT_WORKSPACE_FILE_TOKEN_PATTERN,
181
202
  text,
182
203
  tokens,
183
204
  });
184
- appendWorkspaceTokens({
205
+ appendEncodedKeyTokens({
185
206
  kind: CHAT_WORKSPACE_DIRECTORY_TOKEN_KIND,
186
207
  pattern: CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN,
187
208
  text,
@@ -72,6 +72,9 @@ it("renders panel apps through the shared adaptive inline surface", () => {
72
72
  <ChatInlinePanelAppCard
73
73
  panelApp={{
74
74
  appId: "weather-card",
75
+ params: {
76
+ city: "Shanghai",
77
+ },
75
78
  title: "Weather",
76
79
  }}
77
80
  />,
@@ -99,6 +102,8 @@ it("renders panel apps through the shared adaptive inline surface", () => {
99
102
  expect(iframe.getAttribute("src")).toBe(
100
103
  "/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline",
101
104
  );
105
+ expect(iframe.name).toContain('"city":"Shanghai"');
106
+ expect(iframe.getAttribute("src")).not.toContain("Shanghai");
102
107
 
103
108
  fireEvent(
104
109
  window,
@@ -118,6 +123,9 @@ it("renders panel apps through the shared adaptive inline surface", () => {
118
123
 
119
124
  expect(mocks.openTarget).toHaveBeenCalledWith(
120
125
  expect.objectContaining({
126
+ contentParams: {
127
+ city: "Shanghai",
128
+ },
121
129
  dedupeKey: "panel-app:weather-card",
122
130
  kind: "panel-app",
123
131
  title: "Weather",
@@ -126,6 +134,33 @@ it("renders panel apps through the shared adaptive inline surface", () => {
126
134
  );
127
135
  });
128
136
 
137
+ it("replaces the inline iframe when params change at the same URL", () => {
138
+ const { rerender } = render(
139
+ <ChatInlinePanelAppCard
140
+ panelApp={{
141
+ appId: "weather-card",
142
+ params: { city: "Shanghai" },
143
+ title: "Weather",
144
+ }}
145
+ />,
146
+ );
147
+ const firstIframe = screen.getByTitle("Weather");
148
+
149
+ rerender(
150
+ <ChatInlinePanelAppCard
151
+ panelApp={{
152
+ appId: "weather-card",
153
+ params: { city: "Beijing" },
154
+ title: "Weather",
155
+ }}
156
+ />,
157
+ );
158
+
159
+ const nextIframe = screen.getByTitle("Weather") as HTMLIFrameElement;
160
+ expect(nextIframe).not.toBe(firstIframe);
161
+ expect(nextIframe.name).toContain('"city":"Beijing"');
162
+ });
163
+
129
164
  it("renders and expands a panel app from an explicit external path", () => {
130
165
  render(
131
166
  <ChatInlinePanelAppCard
@@ -453,7 +453,7 @@ it("wires markdown file link actions to the workspace file preview manager", ()
453
453
  expect(captures.openFilePreview).toHaveBeenCalledWith(action);
454
454
  });
455
455
 
456
- it("opens workspace file and directory tokens from the message", () => {
456
+ it("opens workspace and project reference tokens from the message", () => {
457
457
  captures.selectedSession = {
458
458
  projectRoot: "/tmp/project",
459
459
  workingDir: "/tmp/project/packages/ui",
@@ -474,6 +474,12 @@ it("opens workspace file and directory tokens from the message", () => {
474
474
  label: "server",
475
475
  rawText: "@folder:packages%2Fserver",
476
476
  });
477
+ onInlineTokenClick?.({
478
+ kind: "project",
479
+ key: "/tmp/nextclaw",
480
+ label: "NextClaw",
481
+ rawText: "@project:%2Ftmp%2Fnextclaw",
482
+ });
477
483
 
478
484
  expect(captures.openFilePreview).toHaveBeenNthCalledWith(1, {
479
485
  path: "/tmp/project/docs/guide.md",
@@ -485,6 +491,11 @@ it("opens workspace file and directory tokens from the message", () => {
485
491
  label: "server",
486
492
  viewMode: "preview",
487
493
  });
494
+ expect(captures.openFilePreview).toHaveBeenNthCalledWith(3, {
495
+ path: "/tmp/nextclaw",
496
+ label: "NextClaw",
497
+ viewMode: "preview",
498
+ });
488
499
  });
489
500
 
490
501
  it("opens panel app tokens through the existing content owner", () => {
@@ -31,7 +31,7 @@ export function ChatInlineFilePreview({
31
31
  if (display.target.type !== "file") {
32
32
  return null;
33
33
  }
34
- const { path, line, column, viewer } = display.target.payload;
34
+ const { path, line, column, params, viewer } = display.target.payload;
35
35
  const file = createWorkspaceFileTab(
36
36
  {
37
37
  path,
@@ -40,6 +40,7 @@ export function ChatInlineFilePreview({
40
40
  previewViewer: viewer,
41
41
  line,
42
42
  column,
43
+ params,
43
44
  },
44
45
  parentSessionKey,
45
46
  );
@@ -58,6 +59,7 @@ export function ChatInlineFilePreview({
58
59
  previewViewer,
59
60
  line: file.line ?? undefined,
60
61
  column: file.column ?? undefined,
62
+ params: file.params ?? undefined,
61
63
  });
62
64
  };
63
65
 
@@ -19,10 +19,11 @@ import { ChatInlineContentSurface } from "@/features/chat/features/message/compo
19
19
  import { useDocBrowser } from "@/shared/components/doc-browser";
20
20
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
21
21
  import { t } from "@/shared/lib/i18n";
22
+ import { createUiContentParamsWindowName } from "@nextclaw/shared";
22
23
 
23
24
  type ChatInlinePanelAppDescriptor = Pick<
24
25
  ChatPanelAppCardViewModel,
25
- "appId" | "path" | "title"
26
+ "appId" | "params" | "path" | "title"
26
27
  >;
27
28
 
28
29
  export function ChatInlinePanelAppCard({
@@ -60,21 +61,27 @@ export function ChatInlinePanelAppCard({
60
61
  () =>
61
62
  createInlinePanelAppTab({
62
63
  appId: entry?.id ?? panelApp.appId,
64
+ params: panelApp.params,
63
65
  path: panelApp.path,
64
66
  title,
65
67
  url,
66
- }),
67
- [entry?.id, panelApp.appId, panelApp.path, title, url],
68
+ }),
69
+ [entry?.id, panelApp.appId, panelApp.params, panelApp.path, title, url],
68
70
  );
69
- const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}`;
71
+ const contentParamsWindowName = createUiContentParamsWindowName(panelApp.params);
72
+ const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}:${contentParamsWindowName ?? ""}`;
70
73
  const openExpanded = () => {
71
74
  if (target) {
72
- docBrowser.openTarget(target);
75
+ docBrowser.openTarget({
76
+ ...target,
77
+ contentParams: panelApp.params,
78
+ });
73
79
  return;
74
80
  }
75
81
  docBrowser.open(url, {
76
82
  dedupeKey: tab.dedupeKey,
77
83
  kind: tab.kind,
84
+ contentParams: panelApp.params,
78
85
  title,
79
86
  });
80
87
  };
@@ -130,6 +137,7 @@ export function ChatInlinePanelAppCard({
130
137
  ref={iframeRef}
131
138
  key={iframeInstanceId}
132
139
  src={cardUrl}
140
+ name={contentParamsWindowName}
133
141
  title={title}
134
142
  sandbox={PANEL_APP_IFRAME_SANDBOX}
135
143
  tabIndex={0}
@@ -6,6 +6,7 @@ import {
6
6
  type RefObject,
7
7
  } from "react";
8
8
  import type { NcpMessage } from "@nextclaw/ncp";
9
+ import { CHAT_PROJECT_TOKEN_KIND } from "@nextclaw/shared";
9
10
  import { toast } from "sonner";
10
11
  import {
11
12
  type ChatInlineDisplayViewModel,
@@ -145,6 +146,7 @@ function renderChatInlineDisplay(display: ChatInlineDisplayViewModel) {
145
146
  <ChatInlinePanelAppCard
146
147
  panelApp={{
147
148
  appId: display.target.payload.appId,
149
+ params: display.target.payload.params,
148
150
  path: display.target.payload.path,
149
151
  title: display.title,
150
152
  }}
@@ -345,13 +347,16 @@ export function ChatMessageListContainer({
345
347
  }
346
348
  if (
347
349
  "key" in token &&
348
- (token.kind === "workspace_file" ||
350
+ (token.kind === CHAT_PROJECT_TOKEN_KIND ||
351
+ token.kind === "workspace_file" ||
349
352
  token.kind === "workspace_directory")
350
353
  ) {
351
- const path = resolveWorkspaceReferencePath({
352
- projectRoot: selectedSession?.projectRoot,
353
- relativePath: token.key,
354
- });
354
+ const path = token.kind === CHAT_PROJECT_TOKEN_KIND
355
+ ? token.key
356
+ : resolveWorkspaceReferencePath({
357
+ projectRoot: selectedSession?.projectRoot,
358
+ relativePath: token.key,
359
+ });
355
360
  if (path) {
356
361
  presenter.chatThreadManager.openFilePreview({
357
362
  path,