@nextclaw/ui 0.15.11 → 0.15.13

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 (107) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-pbx14YAL.js → api-DGG69ZcJ.js} +3 -3
  3. package/dist/assets/app-presenter-provider-B1RgR5ug.js +106 -0
  4. package/dist/assets/{appearance-settings-page-D9wJdf-0.js → appearance-settings-page-BZSd1SrH.js} +1 -1
  5. package/dist/assets/{book-open-DZyssdCc.js → book-open-BhaS5BJv.js} +1 -1
  6. package/dist/assets/{card-D7l-gWkY.js → card-Bcr5YM8F.js} +1 -1
  7. package/dist/assets/{channels-list-page-B3qW3T-B.js → channels-list-page-C_x83ZCT.js} +2 -2
  8. package/dist/assets/{chat-page-DUI_Jm6r.js → chat-page-D2xfR6TK.js} +1 -1
  9. package/dist/assets/{config-split-page-DFEKStD7.js → config-split-page-RXAoLeNv.js} +1 -1
  10. package/dist/assets/{confirm-dialog-7wpxfMCp.js → confirm-dialog-o_VPKvVD.js} +1 -1
  11. package/dist/assets/{desktop-update-config-DP6IbxBO.js → desktop-update-config-DEOMOsDN.js} +1 -1
  12. package/dist/assets/{dist-B9UN0HrS.js → dist-C87P4QWA.js} +1 -1
  13. package/dist/assets/doc-browser-BuGh646I.js +1 -0
  14. package/dist/assets/{doc-browser-D7klPMHs.js → doc-browser-DQ75iUkA.js} +1 -1
  15. package/dist/assets/{doc-browser-context-Bw9m83Jp.js → doc-browser-context-tE0--wSa.js} +1 -1
  16. package/dist/assets/{eye-IVMB0Rrz.js → eye-DL9QkrKg.js} +1 -1
  17. package/dist/assets/{form-actions-DrFAgO58.js → form-actions-J0tIaUdK.js} +1 -1
  18. package/dist/assets/index-B2YF-7Yw.css +1 -0
  19. package/dist/assets/index-B9QEjnoY.js +2 -0
  20. package/dist/assets/loader-circle-CAnHHSrY.js +1 -0
  21. package/dist/assets/{logo-badge-DoZOVuPr.js → logo-badge-BYTbZbOF.js} +1 -1
  22. package/dist/assets/{mcp-marketplace-page-C151EGP-.js → mcp-marketplace-page-COWlzeP1.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-CaUFv37E.js +1 -0
  24. package/dist/assets/{model-config-page-BmfQzc52.js → model-config-page-oNqnMXNY.js} +1 -1
  25. package/dist/assets/{navigation-link-CJw6FLOj.js → navigation-link-Dk6VdDkc.js} +1 -1
  26. package/dist/assets/plus-DjdTWPjQ.js +1 -0
  27. package/dist/assets/{popover-DitLJE6k.js → popover-Txwrum65.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-CuAtduIc.js → provider-scoped-model-input-B7rQM4b6.js} +1 -1
  29. package/dist/assets/{providers-config-page-CrmuY4DC.js → providers-config-page-CP-OnvLK.js} +1 -1
  30. package/dist/assets/{react-CTDaaQ9V.js → react-B0LbRnAE.js} +1 -1
  31. package/dist/assets/{refresh-cw-E5qTlQY6.js → refresh-cw-C84mMku5.js} +1 -1
  32. package/dist/assets/remote-2gZkJSX4.js +1 -0
  33. package/dist/assets/rotate-cw-DQJhDnjg.js +1 -0
  34. package/dist/assets/{runtime-config-page-1fPJJvo5.js → runtime-config-page-u_fuHxc8.js} +1 -1
  35. package/dist/assets/{save-CTiZvAd2.js → save-R_qyrHpa.js} +1 -1
  36. package/dist/assets/{search-CztGmpnR.js → search-UKxiouNi.js} +1 -1
  37. package/dist/assets/{search-config-page-D3jVALZU.js → search-config-page-DzkpSGFH.js} +1 -1
  38. package/dist/assets/{secrets-config-page-CxWLQCjZ.js → secrets-config-page-DhoLYm7Z.js} +2 -2
  39. package/dist/assets/security-config-yraYoy9s.js +1 -0
  40. package/dist/assets/{select-BPVI2dQ_.js → select-CYHT8P5S.js} +1 -1
  41. package/dist/assets/{settings-2-Cd7QaBsq.js → settings-2-DGh4bGSi.js} +1 -1
  42. package/dist/assets/skeleton-BQuylYFO.js +1 -0
  43. package/dist/assets/{tabs-DrUmYwEQ.js → tabs-BCihm1zK.js} +1 -1
  44. package/dist/assets/{tag-chip-CmViAZKK.js → tag-chip-De0iz4o2.js} +1 -1
  45. package/dist/assets/{trash-2-DMRJ63AB.js → trash-2-Bo3DWiju.js} +1 -1
  46. package/dist/assets/use-config-DCzwgChy.js +1 -0
  47. package/dist/assets/{use-confirm-dialog-Du33bHPz.js → use-confirm-dialog-WZ94CehU.js} +1 -1
  48. package/dist/assets/{use-viewport-layout-D80O1d90.js → use-viewport-layout-CZ-3pMne.js} +1 -1
  49. package/dist/assets/x-NZxXyqW1.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +1 -0
  53. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +4 -4
  54. package/src/features/chat/components/layout/chat-sidebar.tsx +27 -12
  55. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -19
  56. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-slash-commands.test.tsx +79 -0
  57. package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts +55 -0
  58. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +28 -0
  59. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +11 -12
  60. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -7
  61. package/src/features/chat/features/message/components/chat-message-list.container.tsx +1 -0
  62. package/src/features/chat/features/message/utils/__tests__/chat-message-execution-summary.utils.test.ts +27 -15
  63. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +3 -0
  64. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +8 -6
  65. package/src/features/chat/features/message/utils/chat-message-execution-summary.utils.ts +1 -6
  66. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +2 -0
  67. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +0 -1
  68. package/src/features/chat/features/project/components/__tests__/chat-project-add-dialog.test.tsx +116 -0
  69. package/src/features/chat/features/project/components/chat-project-add-dialog.tsx +305 -0
  70. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +3 -2
  71. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  72. package/src/features/chat/features/workspace/components/__tests__/workspace-file-preview-error-text.test.ts +22 -0
  73. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -5
  74. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +24 -0
  75. package/src/features/chat/managers/chat-ui.manager.ts +7 -1
  76. package/src/features/cron/components/cron-task-discovery.tsx +1 -6
  77. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +18 -4
  78. package/src/features/right-panel-resources/index.ts +1 -0
  79. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-route-resolver.utils.test.ts +33 -0
  80. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +14 -1
  81. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +10 -2
  82. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +9 -0
  83. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +8 -2
  84. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +8 -4
  85. package/src/shared/hooks/use-projects.ts +15 -1
  86. package/src/shared/lib/api/projects/project.utils.ts +7 -0
  87. package/src/shared/lib/api/types.ts +1 -1
  88. package/src/shared/lib/api/utils/ncp-session.utils.ts +7 -0
  89. package/src/shared/lib/i18n/locales/en-US/chat.json +26 -9
  90. package/src/shared/lib/i18n/locales/zh-CN/chat.json +26 -9
  91. package/src/shared/lib/resource-uri/utils/__tests__/resource-uri-resolver.utils.test.ts +13 -1
  92. package/src/shared/lib/resource-uri/utils/resource-uri.utils.ts +6 -3
  93. package/dist/assets/app-presenter-provider-Q6u2xBAn.js +0 -106
  94. package/dist/assets/doc-browser-wgEeLkn1.js +0 -1
  95. package/dist/assets/index-D297NGOb.js +0 -2
  96. package/dist/assets/index-DqZoodZL.css +0 -1
  97. package/dist/assets/loader-circle-DDgFo4Kv.js +0 -1
  98. package/dist/assets/mcp-marketplace-page-BtNExfMn.js +0 -1
  99. package/dist/assets/plus-CYxLSfLh.js +0 -1
  100. package/dist/assets/remote-YzPpOPmK.js +0 -1
  101. package/dist/assets/rotate-cw-BXeiDT6C.js +0 -1
  102. package/dist/assets/security-config-DEyj4WBh.js +0 -1
  103. package/dist/assets/skeleton-CUpAXGoe.js +0 -1
  104. package/dist/assets/use-config-gpXy2Iyc.js +0 -1
  105. package/dist/assets/x-CsLMuNFe.js +0 -1
  106. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +0 -67
  107. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +0 -203
@@ -16,8 +16,12 @@ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-lis
16
16
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
17
17
  import { useSystemStatus } from "@/features/system-status";
18
18
  import { useAgents } from "@/shared/hooks/use-agents";
19
- import { useCreateProject, useProjects } from "@/shared/hooks/use-projects";
20
- import type { ProjectCreateRequest } from "@/shared/lib/api";
19
+ import {
20
+ useAddExistingProject,
21
+ useCreateProject,
22
+ useProjects,
23
+ } from "@/shared/hooks/use-projects";
24
+ import type { ProjectAddExistingRequest, ProjectCreateRequest } from "@/shared/lib/api";
21
25
  import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
22
26
  import { cn } from "@/shared/lib/utils";
23
27
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from "@/shared/lib/i18n";
@@ -45,7 +49,7 @@ import {
45
49
  import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
46
50
  import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
47
51
  import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
48
- import { ChatProjectCreateDialog } from "@/features/chat/features/project/components/chat-project-create-dialog";
52
+ import { ChatProjectAddDialog } from "@/features/chat/features/project/components/chat-project-add-dialog";
49
53
 
50
54
  type ChatSidebarVariant = "desktop" | "mobile";
51
55
 
@@ -98,7 +102,7 @@ export function ChatSidebar({
98
102
  (state) => state.isSidebarCollapsed,
99
103
  );
100
104
  const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
101
- const [isProjectCreateOpen, setIsProjectCreateOpen] = useState(false);
105
+ const [isProjectAddOpen, setIsProjectAddOpen] = useState(false);
102
106
  const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
103
107
  const listSnapshot = useChatSessionListStore((state) => state.snapshot);
104
108
  const sessionTypesData = useChatQueryStore(
@@ -111,6 +115,7 @@ export function ChatSidebar({
111
115
  const agentsQuery = useAgents();
112
116
  const projectsQuery = useProjects();
113
117
  const projectCreateMutation = useCreateProject();
118
+ const projectAddExistingMutation = useAddExistingProject();
114
119
  const { isLoading, items } = useNcpSessionListView();
115
120
  const { language, setLanguage } = useI18n();
116
121
  const { theme, setTheme } = useTheme();
@@ -246,13 +251,20 @@ export function ChatSidebar({
246
251
  sessionType,
247
252
  });
248
253
  };
249
- const openProjectCreate = () => {
254
+ const openProjectAdd = () => {
250
255
  projectCreateMutation.reset();
251
- setIsProjectCreateOpen(true);
256
+ projectAddExistingMutation.reset();
257
+ setIsProjectAddOpen(true);
252
258
  };
253
259
  const createProjectFromSidebar = async (input: ProjectCreateRequest): Promise<void> => {
254
260
  await projectCreateMutation.mutateAsync(input);
255
- setIsProjectCreateOpen(false);
261
+ setIsProjectAddOpen(false);
262
+ };
263
+ const addExistingProjectFromSidebar = async (
264
+ input: ProjectAddExistingRequest,
265
+ ): Promise<void> => {
266
+ await projectAddExistingMutation.mutateAsync(input);
267
+ setIsProjectAddOpen(false);
256
268
  };
257
269
 
258
270
  return (
@@ -324,7 +336,7 @@ export function ChatSidebar({
324
336
  isCollapsed={shouldCollapse}
325
337
  isLoading={isLoading}
326
338
  isProjectFirstView={isProjectFirstView}
327
- onCreateProject={openProjectCreate}
339
+ onAddProject={openProjectAdd}
328
340
  onSelectMode={presenter.chatSessionListManager.setListMode}
329
341
  projectGroups={projectGroups}
330
342
  renderSessionItem={renderSessionItem}
@@ -354,16 +366,19 @@ export function ChatSidebar({
354
366
  />
355
367
  ) : null}
356
368
 
357
- <ChatProjectCreateDialog
358
- open={isProjectCreateOpen}
369
+ <ChatProjectAddDialog
370
+ open={isProjectAddOpen}
359
371
  defaultWorkspacePath={normalizeSessionProjectRootValue(
360
372
  config?.agents.defaults.workspace,
361
373
  )}
362
374
  templates={projectsQuery.data?.templates ?? []}
363
375
  isCreating={projectCreateMutation.isPending}
364
- errorMessage={projectCreateMutation.error?.message}
365
- onOpenChange={setIsProjectCreateOpen}
376
+ isAddingExisting={projectAddExistingMutation.isPending}
377
+ createErrorMessage={projectCreateMutation.error?.message}
378
+ addExistingErrorMessage={projectAddExistingMutation.error?.message}
379
+ onOpenChange={setIsProjectAddOpen}
366
380
  onCreate={createProjectFromSidebar}
381
+ onAddExisting={addExistingProjectFromSidebar}
367
382
  />
368
383
  </aside>
369
384
  );
@@ -49,6 +49,7 @@ import {
49
49
  } from '@/features/chat/managers/chat-recent-skills.manager';
50
50
 
51
51
  import { useSessionConversationInputAttachments } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-attachments';
52
+ import { useSessionConversationSlashCommands } from '@/features/chat/features/conversation/hooks/use-session-conversation-slash-commands';
52
53
  import { ChatConversationTrack } from '@/features/chat/components/conversation/chat-conversation-track';
53
54
  import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
54
55
  import type { useSessionConversationInputQuery } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-query';
@@ -222,26 +223,10 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
222
223
  skillGroupLabels: labels.skillGroupLabels,
223
224
  skillScopeLabels: labels.skillScopeLabels,
224
225
  });
225
- const slashCommands = useMemo(() => {
226
- const parentSessionKey = inputQuery.selectedSessionKey?.trim();
227
- if (!parentSessionKey) {
228
- return [];
229
- }
230
- return [
231
- {
232
- key: 'side-chat',
233
- title: t('chatSlashCommandSideChatTitle', language),
234
- description: t('chatSlashCommandSideChatDescription', language),
235
- detailLines: [t('chatSlashCommandSideChatDetail', language)],
236
- keywords: ['side', 'chat', 'child', 'branch', 'new'],
237
- onSelect: () => presenter.chatThreadManager.openSideChatDraft(parentSessionKey),
238
- },
239
- ];
240
- }, [
241
- inputQuery.selectedSessionKey,
226
+ const slashCommands = useSessionConversationSlashCommands({
242
227
  language,
243
- presenter.chatThreadManager,
244
- ]);
228
+ selectedSessionKey: inputQuery.selectedSessionKey,
229
+ });
245
230
  const handleSlashPanelAppSelect = useCallback(
246
231
  (appId: string) =>
247
232
  void presenter.chatUiManager.showContent({
@@ -0,0 +1,79 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { toast } from 'sonner';
4
+ import { compactNcpSessionContext } from '@/shared/lib/api';
5
+ import { useSessionConversationSlashCommands } from '@/features/chat/features/conversation/hooks/use-session-conversation-slash-commands';
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ compactNcpSessionContext: vi.fn(),
9
+ openSideChatDraft: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('sonner', () => ({
13
+ toast: { error: vi.fn(), success: vi.fn() },
14
+ }));
15
+
16
+ vi.mock('@/shared/lib/api', () => ({
17
+ compactNcpSessionContext: mocks.compactNcpSessionContext,
18
+ }));
19
+
20
+ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
21
+ usePresenter: () => ({
22
+ chatThreadManager: { openSideChatDraft: mocks.openSideChatDraft },
23
+ }),
24
+ }));
25
+
26
+ describe('useSessionConversationSlashCommands', () => {
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ mocks.compactNcpSessionContext.mockResolvedValue({
30
+ compacted: true,
31
+ sessionId: 'session-1',
32
+ });
33
+ });
34
+
35
+ it('exposes the runtime compaction command for the selected session', async () => {
36
+ const { result } = renderHook(() => useSessionConversationSlashCommands({
37
+ language: 'en',
38
+ selectedSessionKey: ' session-1 ',
39
+ }));
40
+ const command = result.current.find((entry) => entry.key === 'compact-context');
41
+
42
+ act(() => command?.onSelect());
43
+
44
+ await waitFor(() => expect(compactNcpSessionContext).toHaveBeenCalledWith('session-1'));
45
+ expect(toast.success).toHaveBeenCalledOnce();
46
+ });
47
+
48
+ it('reports runtime errors and suppresses duplicate in-flight requests', async () => {
49
+ let rejectRequest!: (error: Error) => void;
50
+ mocks.compactNcpSessionContext.mockReturnValue(new Promise((_resolve, reject) => {
51
+ rejectRequest = reject;
52
+ }));
53
+ const { result } = renderHook(() => useSessionConversationSlashCommands({
54
+ language: 'en',
55
+ selectedSessionKey: 'session-1',
56
+ }));
57
+ const command = result.current.find((entry) => entry.key === 'compact-context');
58
+
59
+ act(() => {
60
+ command?.onSelect();
61
+ command?.onSelect();
62
+ });
63
+ expect(compactNcpSessionContext).toHaveBeenCalledOnce();
64
+ rejectRequest(new Error('session is busy'));
65
+
66
+ await waitFor(() => expect(toast.error).toHaveBeenCalledWith(
67
+ expect.stringContaining('session is busy'),
68
+ ));
69
+ });
70
+
71
+ it('does not expose session commands before a session exists', () => {
72
+ const { result } = renderHook(() => useSessionConversationSlashCommands({
73
+ language: 'en',
74
+ selectedSessionKey: null,
75
+ }));
76
+
77
+ expect(result.current).toEqual([]);
78
+ });
79
+ });
@@ -0,0 +1,55 @@
1
+ import { useCallback, useMemo, useRef } from 'react';
2
+ import { toast } from 'sonner';
3
+ import { compactNcpSessionContext } from '@/shared/lib/api';
4
+ import { t, type I18nLanguage } from '@/shared/lib/i18n';
5
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
6
+ import type { ChatSlashCommandDescriptor } from '@/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils';
7
+
8
+ export function useSessionConversationSlashCommands(params: {
9
+ language: I18nLanguage;
10
+ selectedSessionKey?: string | null;
11
+ }): readonly ChatSlashCommandDescriptor[] {
12
+ const { language, selectedSessionKey } = params;
13
+ const presenter = usePresenter();
14
+ const compactingSessionIdsRef = useRef(new Set<string>());
15
+ const compactContext = useCallback(async (sessionId: string) => {
16
+ if (compactingSessionIdsRef.current.has(sessionId)) {
17
+ return;
18
+ }
19
+ compactingSessionIdsRef.current.add(sessionId);
20
+ try {
21
+ await compactNcpSessionContext(sessionId);
22
+ toast.success(t('chatSlashCommandCompactContextSuccess', language));
23
+ } catch (error) {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ toast.error(`${t('chatSlashCommandCompactContextFailed', language)}: ${message}`);
26
+ } finally {
27
+ compactingSessionIdsRef.current.delete(sessionId);
28
+ }
29
+ }, [language]);
30
+
31
+ return useMemo(() => {
32
+ const sessionId = selectedSessionKey?.trim();
33
+ if (!sessionId) {
34
+ return [];
35
+ }
36
+ return [
37
+ {
38
+ key: 'side-chat',
39
+ title: t('chatSlashCommandSideChatTitle', language),
40
+ description: t('chatSlashCommandSideChatDescription', language),
41
+ detailLines: [t('chatSlashCommandSideChatDetail', language)],
42
+ keywords: ['side', 'chat', 'child', 'branch', 'new'],
43
+ onSelect: () => presenter.chatThreadManager.openSideChatDraft(sessionId),
44
+ },
45
+ {
46
+ key: 'compact-context',
47
+ title: t('chatSlashCommandCompactContextTitle', language),
48
+ description: t('chatSlashCommandCompactContextDescription', language),
49
+ detailLines: [t('chatSlashCommandCompactContextDetail', language)],
50
+ keywords: ['compact', 'compress', 'context', 'summary', '压缩', '上下文'],
51
+ onSelect: () => void compactContext(sessionId),
52
+ },
53
+ ];
54
+ }, [compactContext, language, presenter.chatThreadManager, selectedSessionKey]);
55
+ }
@@ -126,6 +126,34 @@ it("renders panel apps through the shared adaptive inline surface", () => {
126
126
  );
127
127
  });
128
128
 
129
+ it("renders and expands a panel app from an explicit external path", () => {
130
+ render(
131
+ <ChatInlinePanelAppCard
132
+ panelApp={{
133
+ appId: "weather-card",
134
+ path: "/tmp/weather-card.panel",
135
+ title: "External Weather",
136
+ }}
137
+ />,
138
+ );
139
+
140
+ expect(screen.getByTitle("External Weather").getAttribute("src")).toBe(
141
+ "/api/panel-apps/weather-card/content?path=%2Ftmp%2Fweather-card.panel&nextclawDisplayMode=card&nextclawPlacement=inline",
142
+ );
143
+
144
+ fireEvent.click(screen.getByLabelText("chatPanelCardExpand"));
145
+
146
+ expect(mocks.openTarget).not.toHaveBeenCalled();
147
+ expect(mocks.open).toHaveBeenCalledWith(
148
+ "/api/panel-apps/weather-card/content?path=%2Ftmp%2Fweather-card.panel",
149
+ {
150
+ dedupeKey: "panel-app:/tmp/weather-card.panel",
151
+ kind: "panel-app",
152
+ title: "External Weather",
153
+ },
154
+ );
155
+ });
156
+
129
157
  it("ignores inline height messages from a different window", () => {
130
158
  const { container } = render(
131
159
  <ChatInlinePanelAppCard
@@ -358,7 +358,7 @@ it("derives completed assistant process duration from message lifecycle", () =>
358
358
  });
359
359
  });
360
360
 
361
- it("projects ai execution metadata into the assistant message footer summary", () => {
361
+ it("keeps unavailable ai usage in metadata but out of the footer", () => {
362
362
  const assistantMessage = {
363
363
  id: "assistant-execution",
364
364
  sessionId: "session-1",
@@ -375,13 +375,13 @@ it("projects ai execution metadata into the assistant message footer summary", (
375
375
  requestedModel: null,
376
376
  outcome: "completed",
377
377
  usage: {
378
- inputTokens: 1_200,
379
- outputTokens: 200,
380
- cachedInputTokens: 500,
381
- totalTokens: 1_400,
382
- modelCallCount: 1,
383
- reportedModelCallCount: 1,
384
- status: "reported",
378
+ inputTokens: null,
379
+ outputTokens: null,
380
+ cachedInputTokens: null,
381
+ totalTokens: null,
382
+ modelCallCount: null,
383
+ reportedModelCallCount: null,
384
+ status: "unavailable",
385
385
  },
386
386
  },
387
387
  },
@@ -397,8 +397,7 @@ it("projects ai execution metadata into the assistant message footer summary", (
397
397
  const renderedMessages =
398
398
  captures.renders[captures.renders.length - 1]?.messages ?? [];
399
399
  expect(renderedMessages[0]).toMatchObject({
400
- executionSummaryLabel:
401
- "openai/gpt-5 · 1.2k chatAiExecutionInput / 200 chatAiExecutionOutput",
400
+ executionSummaryLabel: "openai/gpt-5",
402
401
  moreActions: {
403
402
  triggerLabel: "chatMessageMoreActions",
404
403
  items: [
@@ -408,8 +407,8 @@ it("projects ai execution metadata into the assistant message footer summary", (
408
407
  dialog: {
409
408
  rows: expect.arrayContaining([
410
409
  {
411
- label: "chatAiExecutionCachedInputTokens",
412
- value: "500",
410
+ label: "chatAiExecutionUsageStatus",
411
+ value: "chatAiExecutionUsageUnavailable",
413
412
  },
414
413
  ]),
415
414
  },
@@ -22,7 +22,7 @@ import { t } from "@/shared/lib/i18n";
22
22
 
23
23
  type ChatInlinePanelAppDescriptor = Pick<
24
24
  ChatPanelAppCardViewModel,
25
- "appId" | "title"
25
+ "appId" | "path" | "title"
26
26
  >;
27
27
 
28
28
  export function ChatInlinePanelAppCard({
@@ -39,17 +39,19 @@ export function ChatInlinePanelAppCard({
39
39
  const panelApps = usePanelApps();
40
40
  const entry = useMemo(
41
41
  () =>
42
- findPanelAppEntryByDisplayId(
42
+ panelApp.path ? undefined : findPanelAppEntryByDisplayId(
43
43
  panelApps.data?.entries ?? [],
44
44
  panelApp.appId,
45
45
  ),
46
- [panelApp.appId, panelApps.data?.entries],
46
+ [panelApp.appId, panelApp.path, panelApps.data?.entries],
47
47
  );
48
48
  const target = entry ? createPanelAppRightPanelResourceTarget(entry) : null;
49
49
  const title = panelApp.title ?? target?.title ?? panelApp.appId;
50
- const url = panelApps.isLoading
51
- ? ""
52
- : (target?.url ?? createFallbackPanelAppContentPath(panelApp.appId));
50
+ const url = panelApp.path
51
+ ? createFallbackPanelAppContentPath(panelApp.appId, panelApp.path)
52
+ : panelApps.isLoading
53
+ ? ""
54
+ : (target?.url ?? createFallbackPanelAppContentPath(panelApp.appId));
53
55
  const cardUrl = useMemo(
54
56
  () => (url ? createInlinePanelAppCardUrl(url) : ""),
55
57
  [url],
@@ -58,10 +60,11 @@ export function ChatInlinePanelAppCard({
58
60
  () =>
59
61
  createInlinePanelAppTab({
60
62
  appId: entry?.id ?? panelApp.appId,
63
+ path: panelApp.path,
61
64
  title,
62
65
  url,
63
66
  }),
64
- [entry?.id, panelApp.appId, title, url],
67
+ [entry?.id, panelApp.appId, panelApp.path, title, url],
65
68
  );
66
69
  const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}`;
67
70
  const openExpanded = () => {
@@ -144,6 +144,7 @@ function renderChatInlineDisplay(display: ChatInlineDisplayViewModel) {
144
144
  <ChatInlinePanelAppCard
145
145
  panelApp={{
146
146
  appId: display.target.payload.appId,
147
+ path: display.target.payload.path,
147
148
  title: display.title,
148
149
  }}
149
150
  />
@@ -10,7 +10,6 @@ const labels = {
10
10
  output: "output",
11
11
  tokens: "tokens",
12
12
  partial: "partial usage",
13
- unavailable: "Token usage unavailable",
14
13
  };
15
14
 
16
15
  const presentationLabels = {
@@ -85,7 +84,7 @@ describe("buildChatMessageExecutionSummary", () => {
85
84
  ).toBe("openai/gpt-5 · 143k input / 307 output");
86
85
  });
87
86
 
88
- it("shows partial and unavailable states without inventing zero usage", () => {
87
+ it("shows partial usage and keeps unavailable usage out of the footer", () => {
89
88
  const base = {
90
89
  version: 1,
91
90
  runId: "run-1",
@@ -111,23 +110,36 @@ describe("buildChatMessageExecutionSummary", () => {
111
110
  labels,
112
111
  }),
113
112
  ).toBe("gpt-5 · 2k input · partial usage");
113
+ const unavailableExecution = {
114
+ ...base,
115
+ usage: {
116
+ inputTokens: null,
117
+ outputTokens: null,
118
+ cachedInputTokens: null,
119
+ totalTokens: null,
120
+ modelCallCount: null,
121
+ reportedModelCallCount: null,
122
+ status: "unavailable" as const,
123
+ },
124
+ };
114
125
  expect(
115
126
  buildChatMessageExecutionSummary({
116
- message: createMessage({
117
- ...base,
118
- usage: {
119
- inputTokens: null,
120
- outputTokens: null,
121
- cachedInputTokens: null,
122
- totalTokens: null,
123
- modelCallCount: null,
124
- reportedModelCallCount: null,
125
- status: "unavailable",
126
- },
127
- }),
127
+ message: createMessage(unavailableExecution),
128
128
  labels,
129
129
  }),
130
- ).toBe("gpt-5 · Token usage unavailable");
130
+ ).toBe("gpt-5");
131
+
132
+ const presentation = buildChatMessageExecutionPresentation({
133
+ message: createMessage(unavailableExecution),
134
+ labels: presentationLabels,
135
+ });
136
+ expect(presentation?.summaryLabel).toBe("gpt-5");
137
+ expect(presentation?.moreActions.items[0]?.dialog?.rows).toEqual(
138
+ expect.arrayContaining([
139
+ { label: "Input tokens", value: "Not available" },
140
+ { label: "Usage status", value: "Unavailable" },
141
+ ]),
142
+ );
131
143
  });
132
144
 
133
145
  it("keeps cached and exact token counts in the metadata detail action", () => {
@@ -149,6 +149,7 @@ describe("buildShowContentToolCard", () => {
149
149
  type: "panel_app",
150
150
  payload: {
151
151
  appId: "reader",
152
+ path: "/tmp/reader.panel",
152
153
  },
153
154
  },
154
155
  title: "Reader",
@@ -164,6 +165,7 @@ describe("buildShowContentToolCard", () => {
164
165
  expect(card).toMatchObject({
165
166
  panelApp: {
166
167
  appId: "reader",
168
+ path: "/tmp/reader.panel",
167
169
  title: "Reader",
168
170
  action: {
169
171
  kind: "show-content",
@@ -173,6 +175,7 @@ describe("buildShowContentToolCard", () => {
173
175
  type: "panel_app",
174
176
  payload: {
175
177
  appId: "reader",
178
+ path: "/tmp/reader.panel",
176
179
  },
177
180
  },
178
181
  title: "Reader",
@@ -1,12 +1,13 @@
1
1
  import {
2
+ createPanelAppContentPath,
2
3
  createPanelAppResourceUri,
3
4
  RIGHT_PANEL_PANEL_APP_TAB_KIND,
4
5
  } from "@/features/right-panel-resources";
5
6
  import { PANEL_APP_INLINE_HOST_CONTRACT } from "@nextclaw/shared";
6
7
  import type { DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
7
8
 
8
- export function createFallbackPanelAppContentPath(appId: string): string {
9
- return `/api/panel-apps/${encodeURIComponent(appId)}/content`;
9
+ export function createFallbackPanelAppContentPath(appId: string, sourcePath?: string): string {
10
+ return createPanelAppContentPath(appId, sourcePath);
10
11
  }
11
12
 
12
13
  export function createInlinePanelAppCardUrl(url: string): string {
@@ -35,19 +36,20 @@ export function readInlinePanelAppContentHeight(value: unknown): number | null {
35
36
 
36
37
  export function createInlinePanelAppTab(params: {
37
38
  appId: string;
39
+ path?: string;
38
40
  title: string;
39
41
  url: string;
40
42
  }): DocBrowserTab {
41
- const { appId, title, url } = params;
43
+ const { appId, path, title, url } = params;
42
44
  return {
43
45
  currentUrl: url,
44
- dedupeKey: `panel-app:${appId}`,
46
+ dedupeKey: `panel-app:${path ?? appId}`,
45
47
  history: [url],
46
48
  historyIndex: 0,
47
- id: `inline-panel-app:${appId}`,
49
+ id: `inline-panel-app:${path ?? appId}`,
48
50
  kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
49
51
  navVersion: 0,
50
- resourceUri: createPanelAppResourceUri(appId),
52
+ resourceUri: createPanelAppResourceUri(appId, path),
51
53
  title,
52
54
  };
53
55
  }
@@ -10,7 +10,6 @@ export type ChatMessageExecutionSummaryLabels = {
10
10
  output: string;
11
11
  tokens: string;
12
12
  partial: string;
13
- unavailable: string;
14
13
  };
15
14
 
16
15
  export type ChatMessageExecutionPresentationLabels =
@@ -69,10 +68,6 @@ function buildExecutionSummary(
69
68
  execution: NcpAiExecutionMetadata,
70
69
  labels: ChatMessageExecutionSummaryLabels,
71
70
  ): string {
72
- if (execution.usage.status === "unavailable") {
73
- return `${execution.model} · ${labels.unavailable}`;
74
- }
75
-
76
71
  const tokenSegments: string[] = [];
77
72
  if (execution.usage.inputTokens !== null) {
78
73
  tokenSegments.push(
@@ -90,7 +85,7 @@ function buildExecutionSummary(
90
85
  );
91
86
  }
92
87
  if (tokenSegments.length === 0) {
93
- tokenSegments.push(labels.unavailable);
88
+ return execution.model;
94
89
  }
95
90
  const summary = `${execution.model} · ${tokenSegments.join(" / ")}`;
96
91
  return execution.usage.status === "partial"
@@ -121,6 +121,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
121
121
  type: "panel_app",
122
122
  payload: {
123
123
  appId,
124
+ path: readOptionalString(payload.path),
124
125
  },
125
126
  },
126
127
  title,
@@ -178,6 +179,7 @@ export function buildShowContentToolCard(params: {
178
179
  const panelApp = request.target.type === "panel_app" && request.placement === "inline"
179
180
  ? {
180
181
  appId: request.target.payload.appId,
182
+ path: request.target.payload.path,
181
183
  title: request.title,
182
184
  action: showContentAction,
183
185
  }
@@ -107,7 +107,6 @@ export function buildChatMessageExecutionLabels(
107
107
  output: t("chatAiExecutionOutput"),
108
108
  tokens: t("chatAiExecutionTokens"),
109
109
  partial: t("chatAiExecutionPartial"),
110
- unavailable: t("chatAiExecutionUnavailable"),
111
110
  moreActions: t("chatMessageMoreActions"),
112
111
  viewMetadata: t("chatAiExecutionViewMetadata"),
113
112
  metadataTitle: t("chatAiExecutionMetadataTitle"),