@nextclaw/ui 0.18.3 → 0.19.0

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 (125) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/assets/api-Cyxp93Fn.js +13 -0
  3. package/dist/assets/appearance-settings-page-DsMl931U.js +1 -0
  4. package/dist/assets/{book-open-BOJLGVsx.js → book-open-Cl5CzL0g.js} +1 -1
  5. package/dist/assets/{channels-list-page-BPy2mXoD.js → channels-list-page-D_B7JGoZ.js} +2 -2
  6. package/dist/assets/chat-page-D3ZeULTv.js +1 -0
  7. package/dist/assets/config-split-page-C-YVl2mf.js +1 -0
  8. package/dist/assets/{confirm-dialog-Bsg9A62_.js → confirm-dialog-Bz3ieDQ5.js} +2 -2
  9. package/dist/assets/desktop-update-config-DO051-QG.js +1 -0
  10. package/dist/assets/{dist-IBUY9_9h.js → dist-CDPl8mm7.js} +1 -1
  11. package/dist/assets/{dist-DyUY9uQ0.js → dist-HFDWY6-K.js} +1 -1
  12. package/dist/assets/doc-browser-CWhs0JQN.js +1 -0
  13. package/dist/assets/doc-browser-D6H_nPzO.js +1 -0
  14. package/dist/assets/{doc-browser-context-BM6Af8gN.js → doc-browser-context-BKwz_j44.js} +1 -1
  15. package/dist/assets/form-actions-BUZOCfPR.js +1 -0
  16. package/dist/assets/index-DR3GLGme.css +1 -0
  17. package/dist/assets/index-j6DBnvlr.js +109 -0
  18. package/dist/assets/loader-circle-s65a4ZzL.js +1 -0
  19. package/dist/assets/mcp-marketplace-page-BJvzGt7h.js +9 -0
  20. package/dist/assets/mcp-marketplace-page-EpFHeJzw.js +1 -0
  21. package/dist/assets/model-config-page-CBoh0DVh.js +1 -0
  22. package/dist/assets/{navigation-link-DtPZZ_Q_.js → navigation-link-CDMYpkVv.js} +2 -2
  23. package/dist/assets/plus-XDIkqpkt.js +1 -0
  24. package/dist/assets/{provider-scoped-model-input-RWZz36z7.js → provider-scoped-model-input-C2iUtZi7.js} +1 -1
  25. package/dist/assets/providers-config-page-DX1pISTo.js +1 -0
  26. package/dist/assets/react-B7Paq2b3.js +8 -0
  27. package/dist/assets/{refresh-cw-BQHORcAs.js → refresh-cw-XrnNVwuQ.js} +1 -1
  28. package/dist/assets/remote-D_7_OGoj.js +1 -0
  29. package/dist/assets/rotate-cw-BhIWpLaU.js +1 -0
  30. package/dist/assets/runtime-config-page-BVO9B8Ns.js +1 -0
  31. package/dist/assets/{save-BgiS2u-T.js → save-_nSNAYQu.js} +1 -1
  32. package/dist/assets/{search-B6mO7mmo.js → search-0OTHFRMx.js} +1 -1
  33. package/dist/assets/{search-config-page-QS-kqNDZ.js → search-config-page-BwyeUjKt.js} +1 -1
  34. package/dist/assets/secrets-config-page-2O-TzroJ.js +3 -0
  35. package/dist/assets/security-config-BgIIrg1s.js +1 -0
  36. package/dist/assets/{select-DToos7Rq.js → select-BZHvnwQB.js} +2 -2
  37. package/dist/assets/{settings-2-ZAWXrq81.js → settings-2-CM_PvDNk.js} +1 -1
  38. package/dist/assets/skeleton-Bv4MMbH7.js +1 -0
  39. package/dist/assets/{tag-chip-urB3N-kk.js → tag-chip-Ce53PMTL.js} +1 -1
  40. package/dist/assets/x-CCjWN5ys.js +1 -0
  41. package/dist/index.html +20 -20
  42. package/package.json +6 -6
  43. package/src/app/components/layout/sidebar-items.tsx +49 -15
  44. package/src/app/configs/__tests__/app-navigation.config.test.ts +21 -0
  45. package/src/app/configs/app-navigation.config.ts +10 -1
  46. package/src/app/index.tsx +4 -0
  47. package/src/features/chat/components/conversation/chat-conversation-content.tsx +7 -0
  48. package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
  49. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +26 -22
  50. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +2 -0
  51. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +1 -0
  52. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +1 -0
  53. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +1 -3
  54. package/src/features/chat/features/message/components/chat-message-list.container.tsx +15 -1
  55. package/src/features/chat/features/message/components/chat-tool-call-stress-harness.tsx +178 -0
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +21 -0
  57. package/src/features/chat/features/message/utils/__tests__/chat-tool-call-stress-fixture.utils.test.ts +56 -0
  58. package/src/features/chat/features/message/utils/chat-message-core.utils.ts +1 -1
  59. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +3 -10
  60. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +29 -1
  61. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +3 -0
  62. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +3 -28
  63. package/src/features/chat/features/message/utils/chat-tool-call-stress-fixture.utils.ts +122 -0
  64. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-conversation.test.tsx +69 -0
  65. package/src/features/chat/features/ncp/hooks/use-ncp-session-conversation.ts +29 -0
  66. package/src/features/chat/features/ncp/hooks/use-ncp-session-message-history.ts +106 -1
  67. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -12
  68. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +4 -2
  69. package/src/features/panel-apps/components/__tests__/panel-app-list-item.test.tsx +44 -1
  70. package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +82 -0
  71. package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +13 -6
  72. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +7 -1
  73. package/src/features/panel-apps/components/panel-app-icon.tsx +48 -0
  74. package/src/features/panel-apps/components/panel-app-list-item.tsx +19 -28
  75. package/src/features/panel-apps/components/panel-app-main-sidebar-nav.tsx +72 -0
  76. package/src/features/panel-apps/components/panel-app-toolbar.tsx +81 -20
  77. package/src/features/panel-apps/components/panel-apps-list.tsx +13 -26
  78. package/src/features/panel-apps/hooks/__tests__/use-panel-apps.test.tsx +105 -0
  79. package/src/features/panel-apps/hooks/use-panel-app-client-grant.ts +50 -0
  80. package/src/features/panel-apps/hooks/use-panel-app-main-runtime.ts +66 -0
  81. package/src/features/panel-apps/hooks/use-panel-apps.ts +102 -13
  82. package/src/features/panel-apps/index.ts +2 -0
  83. package/src/features/panel-apps/managers/__tests__/panel-app-bridge.manager.test.ts +1 -14
  84. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +9 -5
  85. package/src/features/panel-apps/pages/__tests__/panel-app-main-page.test.tsx +183 -0
  86. package/src/features/panel-apps/pages/panel-app-main-page.tsx +125 -0
  87. package/src/features/panel-apps/utils/__tests__/panel-app-view.utils.test.ts +2 -1
  88. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +5 -4
  89. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +12 -25
  90. package/src/features/right-panel-resources/index.ts +1 -0
  91. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +22 -0
  92. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +31 -2
  93. package/src/main.tsx +24 -11
  94. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +76 -0
  95. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  96. package/src/shared/lib/api/types.ts +3 -0
  97. package/src/shared/lib/api/utils/ncp-session.utils.ts +24 -1
  98. package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
  99. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  100. package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
  101. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  102. package/dist/assets/api-BSsQ0I4l.js +0 -13
  103. package/dist/assets/appearance-settings-page-2ilQwMl0.js +0 -1
  104. package/dist/assets/chat-page-DDR-Q4Kf.js +0 -1
  105. package/dist/assets/config-split-page-aUTKtWJu.js +0 -1
  106. package/dist/assets/desktop-update-config-Ds4pN7kz.js +0 -1
  107. package/dist/assets/doc-browser-D0arfzHY.js +0 -1
  108. package/dist/assets/doc-browser-D73yN8ts.js +0 -1
  109. package/dist/assets/form-actions-Cpd7glh8.js +0 -1
  110. package/dist/assets/index-Bsoscjk7.css +0 -1
  111. package/dist/assets/index-C2N1bYyb.js +0 -109
  112. package/dist/assets/loader-circle-C1fL3kBZ.js +0 -1
  113. package/dist/assets/mcp-marketplace-page-D8V0GU4Y.js +0 -9
  114. package/dist/assets/mcp-marketplace-page-q6T-vKoH.js +0 -1
  115. package/dist/assets/model-config-page-B7QR33ED.js +0 -1
  116. package/dist/assets/plus-B9V7Z_Eu.js +0 -1
  117. package/dist/assets/providers-config-page-B5yakiZe.js +0 -1
  118. package/dist/assets/react-D-jOo51s.js +0 -8
  119. package/dist/assets/remote-BfRYDhkz.js +0 -1
  120. package/dist/assets/rotate-cw-DuCF-3PX.js +0 -1
  121. package/dist/assets/runtime-config-page-Big45YWb.js +0 -1
  122. package/dist/assets/secrets-config-page-DE3kmDHk.js +0 -3
  123. package/dist/assets/security-config-Ds_2SXBh.js +0 -1
  124. package/dist/assets/skeleton-Bs2eO2CA.js +0 -1
  125. package/dist/assets/x-BkjTUiww.js +0 -1
@@ -14,6 +14,7 @@ import type {
14
14
  } from '@/features/right-panel-resources/types/right-panel-resource.types';
15
15
  import {
16
16
  createPanelAppContentPath,
17
+ readPanelAppIdFromParsedResourceUri,
17
18
  RIGHT_PANEL_APPS_URL,
18
19
  RIGHT_PANEL_APPS_TAB_KIND,
19
20
  RIGHT_PANEL_HOME_TAB_KIND,
@@ -58,39 +59,25 @@ function createDocsResourceUriFromUrl(url: string): string {
58
59
  return createDocsResourceUriFromSegments(pathname.split('/'));
59
60
  }
60
61
 
61
- function getPanelAppId(uri: ParsedResourceUri): string {
62
- if (uri.scheme === 'nextclaw' && uri.authority === 'panel-app') {
63
- return uri.pathSegments[0] ?? '';
64
- }
65
- const [apiSegment, collectionSegment, appId, contentSegment] = uri.pathSegments;
66
- return apiSegment === 'api'
67
- && collectionSegment === 'panel-apps'
68
- && appId !== undefined
69
- && appId.length > 0
70
- && contentSegment === 'content'
71
- ? appId
72
- : '';
73
- }
74
-
75
62
  function isPanelAppContentUri(uri: ParsedResourceUri): boolean {
76
- return getPanelAppId(uri).length > 0;
63
+ return readPanelAppIdFromParsedResourceUri(uri) !== null;
77
64
  }
78
65
 
79
66
  function resolvePanelAppPlaceholderUrl(uri: ParsedResourceUri): string {
80
- const appId = getPanelAppId(uri);
67
+ const appId = readPanelAppIdFromParsedResourceUri(uri);
81
68
  if (!appId) {
82
69
  return 'nextclaw://panel-app';
83
70
  }
84
71
  const path = uri.searchParams.get('path')?.trim();
85
- return createPanelAppContentPath(decodeURIComponent(appId), path);
72
+ return createPanelAppContentPath(appId, path);
86
73
  }
87
74
 
88
75
  function createPanelAppResourceUri(uri: ParsedResourceUri): string {
89
- const appId = getPanelAppId(uri);
76
+ const appId = readPanelAppIdFromParsedResourceUri(uri);
90
77
  if (!appId) {
91
78
  return 'nextclaw://panel-app';
92
79
  }
93
- const resourceUri = `nextclaw://panel-app/${encodeURIComponent(decodeURIComponent(appId))}`;
80
+ const resourceUri = `nextclaw://panel-app/${encodeURIComponent(appId)}`;
94
81
  const path = uri.searchParams.get('path')?.trim();
95
82
  return path ? `${resourceUri}?${new URLSearchParams({ path }).toString()}` : resourceUri;
96
83
  }
@@ -98,12 +85,12 @@ function createPanelAppResourceUri(uri: ParsedResourceUri): string {
98
85
  function arePanelAppUrlsEquivalent(left: string, right: string): boolean {
99
86
  const leftUri = parseResourceUri(left);
100
87
  const rightUri = parseResourceUri(right);
101
- const leftAppId = getPanelAppId(leftUri);
102
- const rightAppId = getPanelAppId(rightUri);
88
+ const leftAppId = readPanelAppIdFromParsedResourceUri(leftUri);
89
+ const rightAppId = readPanelAppIdFromParsedResourceUri(rightUri);
103
90
  const leftPath = leftUri.searchParams.get('path')?.trim() ?? '';
104
91
  const rightPath = rightUri.searchParams.get('path')?.trim() ?? '';
105
- return leftAppId.length > 0 && rightAppId.length > 0
106
- ? decodeURIComponent(leftAppId) === decodeURIComponent(rightAppId) && leftPath === rightPath
92
+ return leftAppId !== null && rightAppId !== null
93
+ ? leftAppId === rightAppId && leftPath === rightPath
107
94
  : left === right;
108
95
  }
109
96
 
@@ -181,10 +168,10 @@ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefi
181
168
  match: (uri) => (uri.scheme === 'nextclaw' && uri.authority === 'panel-app') || isPanelAppContentUri(uri),
182
169
  resolve: (uri) => {
183
170
  const url = resolvePanelAppPlaceholderUrl(uri);
184
- const appId = getPanelAppId(uri);
171
+ const appId = readPanelAppIdFromParsedResourceUri(uri);
185
172
  return {
186
173
  dedupeKey: appId
187
- ? `panel-app:${uri.searchParams.get('path')?.trim() || decodeURIComponent(appId)}`
174
+ ? `panel-app:${uri.searchParams.get('path')?.trim() || appId}`
188
175
  : undefined,
189
176
  historyPolicy: 'managed',
190
177
  kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
@@ -10,6 +10,7 @@ export {
10
10
  createPanelAppContentPath,
11
11
  createPanelAppResourceUri,
12
12
  createPanelAppRightPanelResourceTarget,
13
+ readPanelAppIdFromResourceUri,
13
14
  RIGHT_PANEL_APPS_TAB_KIND,
14
15
  RIGHT_PANEL_APPS_URL,
15
16
  RIGHT_PANEL_HOME_TAB_KIND,
@@ -5,6 +5,7 @@ import {
5
5
  createPanelAppContentPath,
6
6
  createPanelAppResourceUri,
7
7
  createPanelAppRightPanelResourceTarget,
8
+ readPanelAppIdFromResourceUri,
8
9
  } from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
9
10
 
10
11
  function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
@@ -15,6 +16,7 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
15
16
  contentPath: '/api/panel-apps/demo/content',
16
17
  createdAt: '2026-06-02T00:00:00.000Z',
17
18
  favorite: false,
19
+ mainSidebar: false,
18
20
  fileName: 'demo.html',
19
21
  id: 'demo',
20
22
  kind: 'single-file',
@@ -69,6 +71,13 @@ describe('right-panel-resource uri utils', () => {
69
71
  );
70
72
  });
71
73
 
74
+ it('reads stable panel app ids from resource and content URLs', () => {
75
+ expect(readPanelAppIdFromResourceUri('nextclaw://panel-app/publisher.todo')).toBe('publisher.todo');
76
+ expect(readPanelAppIdFromResourceUri('/api/panel-apps/publisher.todo/content')).toBe('publisher.todo');
77
+ expect(readPanelAppIdFromResourceUri('nextclaw://panel-app/publisher.todo?path=%2Ftmp%2Ftodo.panel')).toBeNull();
78
+ expect(readPanelAppIdFromResourceUri('nextclaw://apps')).toBeNull();
79
+ });
80
+
72
81
  it('keeps panel app image icons as dock image icons', () => {
73
82
  const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
74
83
  icon: '/api/panel-apps/demo/assets/icon.png',
@@ -90,4 +99,17 @@ describe('right-panel-resource uri utils', () => {
90
99
  value: '🧭',
91
100
  });
92
101
  });
102
+
103
+ it('uses the stable app id for persistent panel app resources', () => {
104
+ const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
105
+ id: 'installed-directory-name',
106
+ appId: 'publisher.stable-panel',
107
+ contentPath: '/api/panel-apps/publisher.stable-panel/content',
108
+ }));
109
+
110
+ expect(target).toEqual(expect.objectContaining({
111
+ dedupeKey: 'panel-app:publisher.stable-panel',
112
+ resourceUri: 'nextclaw://panel-app/publisher.stable-panel',
113
+ }));
114
+ });
93
115
  });
@@ -6,6 +6,7 @@ import type { PanelAppEntryView } from '@/shared/lib/api';
6
6
  import type { DocBrowserDockIcon } from '@/shared/components/doc-browser';
7
7
  import type { DocBrowserTab } from '@/shared/components/doc-browser/doc-browser-context';
8
8
  import type { RightPanelResourceTarget } from '@/features/right-panel-resources/types/right-panel-resource.types';
9
+ import { parseResourceUri, type ParsedResourceUri } from '@/shared/lib/resource-uri';
9
10
 
10
11
  export const RIGHT_PANEL_HOME_TAB_KIND = 'home';
11
12
  export const RIGHT_PANEL_HOME_URL = 'nextclaw://new-tab';
@@ -78,6 +79,34 @@ export function createPanelAppContentPath(appId: string, sourcePath?: string): s
78
79
  return path ? `${url}?${new URLSearchParams({ path }).toString()}` : url;
79
80
  }
80
81
 
82
+ export function readPanelAppIdFromParsedResourceUri(uri: ParsedResourceUri): string | null {
83
+ const encodedAppId = uri.scheme === 'nextclaw' && uri.authority === 'panel-app'
84
+ ? uri.pathSegments[0]
85
+ : (() => {
86
+ const [apiSegment, collectionSegment, appId, contentSegment] = uri.pathSegments;
87
+ return apiSegment === 'api'
88
+ && collectionSegment === 'panel-apps'
89
+ && contentSegment === 'content'
90
+ ? appId
91
+ : undefined;
92
+ })();
93
+ if (!encodedAppId) {
94
+ return null;
95
+ }
96
+ try {
97
+ return decodeURIComponent(encodedAppId);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ export function readPanelAppIdFromResourceUri(value: string): string | null {
104
+ const uri = parseResourceUri(value);
105
+ return uri.searchParams.has('path')
106
+ ? null
107
+ : readPanelAppIdFromParsedResourceUri(uri);
108
+ }
109
+
81
110
  function isPanelAppImageIcon(icon: string): boolean {
82
111
  return (
83
112
  icon.startsWith('data:image/') ||
@@ -99,11 +128,11 @@ function createPanelAppDockIcon(entry: PanelAppEntryView): DocBrowserDockIcon |
99
128
 
100
129
  export function createPanelAppRightPanelResourceTarget(entry: PanelAppEntryView): RightPanelResourceTarget {
101
130
  return {
102
- dedupeKey: `panel-app:${entry.id}`,
131
+ dedupeKey: `panel-app:${entry.appId}`,
103
132
  dockIcon: createPanelAppDockIcon(entry),
104
133
  historyPolicy: 'managed',
105
134
  kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
106
- resourceUri: createPanelAppResourceUri(entry.id),
135
+ resourceUri: createPanelAppResourceUri(entry.appId),
107
136
  title: entry.title,
108
137
  url: entry.contentPath,
109
138
  };
package/src/main.tsx CHANGED
@@ -7,15 +7,28 @@ import { ThemeProvider } from '@/components/providers/ThemeProvider';
7
7
  import './index.css';
8
8
 
9
9
  const AppRouter = window.nextclawDesktop?.platform === 'win32' ? MemoryRouter : BrowserRouter;
10
+ const root = createRoot(document.getElementById('root')!);
10
11
 
11
- createRoot(document.getElementById('root')!).render(
12
- <StrictMode>
13
- <ThemeProvider>
14
- <I18nProvider>
15
- <AppRouter>
16
- <App />
17
- </AppRouter>
18
- </I18nProvider>
19
- </ThemeProvider>
20
- </StrictMode>
21
- );
12
+ if (import.meta.env.DEV && window.location.pathname === '/__debug/chat-tool-call-stress') {
13
+ void import('@/features/chat/features/message/components/chat-tool-call-stress-harness').then(
14
+ ({ ChatToolCallStressHarness }) => {
15
+ root.render(
16
+ <StrictMode>
17
+ <ChatToolCallStressHarness />
18
+ </StrictMode>,
19
+ );
20
+ },
21
+ );
22
+ } else {
23
+ root.render(
24
+ <StrictMode>
25
+ <ThemeProvider>
26
+ <I18nProvider>
27
+ <AppRouter>
28
+ <App />
29
+ </AppRouter>
30
+ </I18nProvider>
31
+ </ThemeProvider>
32
+ </StrictMode>,
33
+ );
34
+ }
@@ -14,11 +14,43 @@ const { navigateMock } = vi.hoisted(() => ({
14
14
  navigateMock: vi.fn(),
15
15
  }));
16
16
 
17
+ const panelAppHooks = vi.hoisted(() => ({
18
+ mutate: vi.fn(),
19
+ }));
20
+
17
21
  vi.mock("react-router-dom", async () => ({
18
22
  ...((await vi.importActual("react-router-dom")) as object),
19
23
  useNavigate: () => navigateMock,
20
24
  }));
21
25
 
26
+ vi.mock("@/features/panel-apps/hooks/use-panel-apps", async (importOriginal) => ({
27
+ ...(await importOriginal<object>()),
28
+ usePanelApps: () => ({
29
+ data: {
30
+ entries: [{
31
+ appId: "piano",
32
+ clientDeclared: false,
33
+ clientGranted: false,
34
+ contentPath: "/api/panel-apps/piano/content",
35
+ createdAt: "2026-08-19T00:00:00.000Z",
36
+ favorite: false,
37
+ fileName: "piano.panel.html",
38
+ id: "piano",
39
+ kind: "single-file",
40
+ mainSidebar: false,
41
+ openCount: 0,
42
+ sizeBytes: 10,
43
+ title: "Piano",
44
+ updatedAt: "2026-08-19T00:00:00.000Z",
45
+ }],
46
+ },
47
+ }),
48
+ useUpdatePanelAppPreferences: () => ({
49
+ isPending: false,
50
+ mutate: panelAppHooks.mutate,
51
+ }),
52
+ }));
53
+
22
54
  const { docBrowserState } = vi.hoisted<{
23
55
  docBrowserState: DocBrowserContextValue;
24
56
  }>(() => ({
@@ -463,6 +495,50 @@ describe("DocBrowser", () => {
463
495
  });
464
496
  });
465
497
 
498
+ describe("DocBrowser panel app navigation", () => {
499
+ beforeEach(() => {
500
+ resetDocBrowserTestState();
501
+ installDocBrowserPointerTestEnvironment();
502
+ });
503
+
504
+ it("uses browser history as the only back contract for panel apps", async () => {
505
+ const user = userEvent.setup();
506
+ const panelAppTab: DocBrowserTab = {
507
+ id: "piano",
508
+ kind: "panel-app" as const,
509
+ title: "Piano",
510
+ currentUrl: "/api/panel-apps/piano/content",
511
+ resourceUri: "nextclaw://panel-app/piano",
512
+ history: ["/api/panel-apps/piano/content"],
513
+ historyIndex: 0,
514
+ navVersion: 0,
515
+ };
516
+ docBrowserState.tabs = [panelAppTab];
517
+ docBrowserState.activeTabId = panelAppTab.id;
518
+ docBrowserState.activeHistory = [
519
+ { kind: "apps", tabId: "apps", url: "nextclaw://apps?tab=panel-apps" },
520
+ { kind: "panel-app", tabId: "piano", resourceUri: "nextclaw://panel-app/piano", url: panelAppTab.currentUrl },
521
+ ];
522
+ docBrowserState.activeHistoryIndex = 1;
523
+ docBrowserState.currentTab = panelAppTab;
524
+
525
+ render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
526
+
527
+ expect(screen.getAllByRole("button", { name: "Back" })).toHaveLength(1);
528
+ expect(screen.queryByRole("button", { name: "Apps" })).toBeNull();
529
+ await user.click(screen.getByRole("button", { name: "Back" }));
530
+ expect(docBrowserState.goBack).toHaveBeenCalledTimes(1);
531
+
532
+ expect(screen.queryByRole("button", { name: "Add to main sidebar" })).toBeNull();
533
+ await user.click(screen.getByRole("button", { name: "More panel app actions" }));
534
+ await user.click(screen.getByRole("button", { name: "Add to main sidebar" }));
535
+ expect(panelAppHooks.mutate).toHaveBeenCalledWith({
536
+ id: "piano",
537
+ preferences: { mainSidebar: true },
538
+ });
539
+ });
540
+ });
541
+
466
542
  describe("DocBrowser scroll restoration", () => {
467
543
  beforeEach(() => {
468
544
  resetDocBrowserTestState();
@@ -111,6 +111,7 @@ export type NcpSessionMessagesView = {
111
111
  sessionId: string;
112
112
  status: NcpSessionStatus;
113
113
  messages: NcpMessageView[];
114
+ deferredToolPayloads?: Record<string, { cursor: string }>;
114
115
  contextWindow?: SessionContextWindowView | null;
115
116
  total: number;
116
117
  pageInfo: NcpSessionMessagePageInfo;
@@ -467,6 +467,8 @@ export type PanelAppEntryView = {
467
467
  updatedAt: string;
468
468
  sizeBytes: number;
469
469
  favorite: boolean;
470
+ mainSidebar: boolean;
471
+ mainSidebarOrder?: number;
470
472
  clientDeclared: boolean;
471
473
  clientGranted: boolean;
472
474
  lastOpenedAt?: string;
@@ -484,6 +486,7 @@ export type PanelAppListView = {
484
486
 
485
487
  export type PanelAppPreferencesUpdateView = {
486
488
  favorite?: boolean;
489
+ mainSidebar?: boolean;
487
490
  };
488
491
 
489
492
  export type {
@@ -19,7 +19,12 @@ export async function fetchNcpSessions(params?: {
19
19
  // GET /api/ncp/sessions/:sessionId/messages
20
20
  export async function fetchNcpSessionMessages(
21
21
  sessionId: string,
22
- options: { limit?: number; cursor?: string; signal?: AbortSignal } = {},
22
+ options: {
23
+ limit?: number;
24
+ cursor?: string;
25
+ toolPayload?: "summary";
26
+ signal?: AbortSignal;
27
+ } = {},
23
28
  ): Promise<NcpSessionMessagesView> {
24
29
  return (await nextclawClient.sessions.listMessages(
25
30
  sessionId,
@@ -27,6 +32,24 @@ export async function fetchNcpSessionMessages(
27
32
  )) as NcpSessionMessagesView;
28
33
  }
29
34
 
35
+ export async function fetchNcpSessionMessageDetail(
36
+ sessionId: string,
37
+ messageId: string,
38
+ cursor: string,
39
+ signal?: AbortSignal,
40
+ ): Promise<NcpSessionMessagesView["messages"][number]> {
41
+ const response = await fetchNcpSessionMessages(sessionId, {
42
+ cursor,
43
+ limit: 1,
44
+ ...(signal ? { signal } : {}),
45
+ });
46
+ const message = response.messages[0];
47
+ if (!message || message.id !== messageId) {
48
+ throw new Error(`Session message detail no longer matches ${messageId}.`);
49
+ }
50
+ return message;
51
+ }
52
+
30
53
  // GET /api/ncp/sessions/:sessionId/usage
31
54
  export async function fetchNcpSessionTokenUsage(
32
55
  sessionId: string,
@@ -219,6 +219,10 @@
219
219
  "chatHistoryLoadFailed": "Couldn’t load earlier messages",
220
220
  "chatHistoryRetry": "Retry",
221
221
  "chatProcessSummaryProcessed": "Processed",
222
+ "chatProcessSummaryToolCalls": "{count} tool calls",
223
+ "chatToolPayloadLoading": "Loading details",
224
+ "chatToolPayloadLoadFailed": "Couldn’t load details. Try again",
225
+ "chatToolActivityShowMore": "Show next {count} ({remaining} remaining)",
222
226
  "chatAiExecutionInput": "input",
223
227
  "chatAiExecutionOutput": "output",
224
228
  "chatAiExecutionTokens": "tokens",
@@ -165,6 +165,16 @@
165
165
  "panelAppsFavorite": "Favorite panel app",
166
166
  "panelAppsUnfavorite": "Unfavorite panel app",
167
167
  "panelAppsMoreActions": "More panel app actions",
168
+ "panelAppsAddToMainSidebar": "Add to main sidebar",
169
+ "panelAppsRemoveFromMainSidebar": "Remove from main sidebar",
170
+ "panelAppsPreferenceUpdateFailed": "Could not update panel app preferences",
171
+ "panelAppsOpenInRightPanel": "Open in right panel",
172
+ "panelAppsOpenApps": "Open app management",
173
+ "panelAppsMainUnavailable": "This panel app is unavailable. It may be disabled, uninstalled, or removed from the workspace.",
174
+ "panelAppsCheckingPermission": "Checking this panel app's permissions…",
175
+ "panelAppsPermissionDenied": "The required NextClaw Client permission was not granted to this panel app.",
176
+ "panelAppsPermissionFailed": "NextClaw could not confirm this panel app's permissions.",
177
+ "panelAppsTryAgain": "Try again",
168
178
  "panelAppsDelete": "Delete panel app",
169
179
  "panelAppsDeleteConfirmTitle": "Delete panel app?",
170
180
  "panelAppsDeleteConfirmDescription": "This will delete the panel app file and cannot be undone.",
@@ -219,6 +219,10 @@
219
219
  "chatHistoryLoadFailed": "较早消息加载失败",
220
220
  "chatHistoryRetry": "重试",
221
221
  "chatProcessSummaryProcessed": "已处理",
222
+ "chatProcessSummaryToolCalls": "{count} 次工具调用",
223
+ "chatToolPayloadLoading": "正在加载详情",
224
+ "chatToolPayloadLoadFailed": "详情加载失败,点击重试",
225
+ "chatToolActivityShowMore": "继续显示 {count} 项(剩余 {remaining} 项)",
222
226
  "chatAiExecutionInput": "输入",
223
227
  "chatAiExecutionOutput": "输出",
224
228
  "chatAiExecutionTokens": "Token",
@@ -165,6 +165,16 @@
165
165
  "panelAppsFavorite": "收藏面板应用",
166
166
  "panelAppsUnfavorite": "取消收藏面板应用",
167
167
  "panelAppsMoreActions": "更多面板应用操作",
168
+ "panelAppsAddToMainSidebar": "添加到主侧栏",
169
+ "panelAppsRemoveFromMainSidebar": "从主侧栏移除",
170
+ "panelAppsPreferenceUpdateFailed": "无法更新面板应用设置",
171
+ "panelAppsOpenInRightPanel": "在右侧面板打开",
172
+ "panelAppsOpenApps": "打开应用管理",
173
+ "panelAppsMainUnavailable": "此面板应用当前不可用。它可能已被禁用、卸载或从工作区删除。",
174
+ "panelAppsCheckingPermission": "正在确认此面板应用的权限…",
175
+ "panelAppsPermissionDenied": "未授予此面板应用所需的 NextClaw Client 权限。",
176
+ "panelAppsPermissionFailed": "无法确认此面板应用的权限。",
177
+ "panelAppsTryAgain": "重试",
168
178
  "panelAppsDelete": "删除面板应用",
169
179
  "panelAppsDeleteConfirmTitle": "删除面板应用?",
170
180
  "panelAppsDeleteConfirmDescription": "将删除这个面板应用文件,删除后不可恢复。",