@nextclaw/ui 0.18.4 → 0.19.1

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 (126) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/assets/api-Cyxp93Fn.js +13 -0
  3. package/dist/assets/appearance-settings-page-NlQIHbXh.js +1 -0
  4. package/dist/assets/{book-open-BOJLGVsx.js → book-open-Cl5CzL0g.js} +1 -1
  5. package/dist/assets/{channels-list-page-DKrUSeNz.js → channels-list-page-DhXfsjxF.js} +2 -2
  6. package/dist/assets/chat-page-C4DHLN6T.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-CpsXBUJK.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-Bfr0I0Ga.js +109 -0
  17. package/dist/assets/index-DcqrfCMy.css +1 -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-BRScCkS-.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-BFzyywsF.js → provider-scoped-model-input-CD93ZOgZ.js} +1 -1
  25. package/dist/assets/providers-config-page-CFdgth2A.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-DDN70hyU.js +1 -0
  29. package/dist/assets/rotate-cw-BhIWpLaU.js +1 -0
  30. package/dist/assets/runtime-config-page-BxJlm6qT.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-ydeDD1OQ.js → search-config-page-2VAarRcG.js} +1 -1
  34. package/dist/assets/secrets-config-page-KoFrETds.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 +5 -5
  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 +57 -2
  70. package/src/features/panel-apps/components/__tests__/panel-app-main-sidebar-nav.test.tsx +133 -0
  71. package/src/features/panel-apps/components/__tests__/panel-app-toolbar.test.tsx +68 -7
  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 +7 -29
  75. package/src/features/panel-apps/components/panel-app-main-sidebar-menu-item.tsx +37 -0
  76. package/src/features/panel-apps/components/panel-app-main-sidebar-nav.tsx +123 -0
  77. package/src/features/panel-apps/components/panel-app-toolbar.tsx +61 -20
  78. package/src/features/panel-apps/components/panel-apps-list.tsx +5 -27
  79. package/src/features/panel-apps/hooks/__tests__/use-panel-apps.test.tsx +105 -0
  80. package/src/features/panel-apps/hooks/use-panel-app-client-grant.ts +50 -0
  81. package/src/features/panel-apps/hooks/use-panel-app-main-runtime.ts +66 -0
  82. package/src/features/panel-apps/hooks/use-panel-apps.ts +102 -13
  83. package/src/features/panel-apps/index.ts +2 -0
  84. package/src/features/panel-apps/managers/__tests__/panel-app-bridge.manager.test.ts +1 -14
  85. package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +9 -5
  86. package/src/features/panel-apps/pages/__tests__/panel-app-main-page.test.tsx +183 -0
  87. package/src/features/panel-apps/pages/panel-app-main-page.tsx +125 -0
  88. package/src/features/panel-apps/utils/__tests__/panel-app-view.utils.test.ts +2 -1
  89. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +5 -4
  90. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +12 -25
  91. package/src/features/right-panel-resources/index.ts +2 -0
  92. package/src/features/right-panel-resources/utils/__tests__/right-panel-resource-uri.utils.test.ts +21 -0
  93. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +37 -0
  94. package/src/main.tsx +24 -11
  95. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +102 -0
  96. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  97. package/src/shared/lib/api/types.ts +3 -0
  98. package/src/shared/lib/api/utils/ncp-session.utils.ts +24 -1
  99. package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
  100. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  101. package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
  102. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  103. package/dist/assets/api-BSsQ0I4l.js +0 -13
  104. package/dist/assets/appearance-settings-page-CEPxG1w0.js +0 -1
  105. package/dist/assets/chat-page-sGhIjLSr.js +0 -1
  106. package/dist/assets/config-split-page-aUTKtWJu.js +0 -1
  107. package/dist/assets/desktop-update-config-VgvIATTT.js +0 -1
  108. package/dist/assets/doc-browser-D0arfzHY.js +0 -1
  109. package/dist/assets/doc-browser-D73yN8ts.js +0 -1
  110. package/dist/assets/form-actions-Cpd7glh8.js +0 -1
  111. package/dist/assets/index-Bsoscjk7.css +0 -1
  112. package/dist/assets/index-CtPLj_hk.js +0 -109
  113. package/dist/assets/loader-circle-C1fL3kBZ.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-D8V0GU4Y.js +0 -9
  115. package/dist/assets/mcp-marketplace-page-q6T-vKoH.js +0 -1
  116. package/dist/assets/model-config-page-DQ-iQFSO.js +0 -1
  117. package/dist/assets/plus-B9V7Z_Eu.js +0 -1
  118. package/dist/assets/providers-config-page-DUYwVuYe.js +0 -1
  119. package/dist/assets/react-D-jOo51s.js +0 -8
  120. package/dist/assets/remote-CNFQ6bFY.js +0 -1
  121. package/dist/assets/rotate-cw-DuCF-3PX.js +0 -1
  122. package/dist/assets/runtime-config-page-CaWoWoQk.js +0 -1
  123. package/dist/assets/secrets-config-page-BZYqw8BF.js +0 -3
  124. package/dist/assets/security-config-Ds_2SXBh.js +0 -1
  125. package/dist/assets/skeleton-Bs2eO2CA.js +0 -1
  126. package/dist/assets/x-BkjTUiww.js +0 -1
@@ -0,0 +1,125 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { Boxes, RotateCcw } from "lucide-react";
3
+ import { useParams } from "react-router-dom";
4
+ import { useAppPresenter } from "@/app/components/app-presenter-provider";
5
+ import { usePanelAppMainRuntime } from "@/features/panel-apps/hooks/use-panel-app-main-runtime";
6
+ import {
7
+ usePanelApps,
8
+ } from "@/features/panel-apps/hooks/use-panel-apps";
9
+ import {
10
+ PANEL_APP_IFRAME_SANDBOX,
11
+ focusPanelAppIframe,
12
+ } from "@/features/panel-apps/utils/panel-app-iframe.utils";
13
+ import { openApps } from "@/features/panel-apps/utils/panel-app-doc-browser.utils";
14
+ import { useDocBrowser } from "@/shared/components/doc-browser";
15
+ import { t } from "@/shared/lib/i18n";
16
+
17
+ export function PanelAppMainPage() {
18
+ const { appId = "" } = useParams<{ appId: string }>();
19
+ const presenter = useAppPresenter();
20
+ const docBrowser = useDocBrowser();
21
+ const panelApps = usePanelApps();
22
+ const iframeRef = useRef<HTMLIFrameElement>(null);
23
+ const entry = useMemo(
24
+ () => panelApps.data?.entries.find((candidate) => candidate.appId === appId),
25
+ [appId, panelApps.data?.entries],
26
+ );
27
+ const runtime = usePanelAppMainRuntime(entry);
28
+
29
+ useEffect(() => {
30
+ const handleMessage = (event: MessageEvent) => {
31
+ presenter.panelAppBridgeManager.handleIframeMessage({
32
+ event,
33
+ iframe: iframeRef.current,
34
+ });
35
+ };
36
+ window.addEventListener("message", handleMessage);
37
+ return () => window.removeEventListener("message", handleMessage);
38
+ }, [presenter]);
39
+
40
+ if (panelApps.isLoading) {
41
+ return <PanelAppMainStatus message={t("panelAppsLoading")} />;
42
+ }
43
+
44
+ if (panelApps.isError) {
45
+ return (
46
+ <PanelAppMainStatus
47
+ message={panelApps.error instanceof Error
48
+ ? panelApps.error.message
49
+ : t("panelAppsLoadFailed")}
50
+ actionLabel={t("panelAppsTryAgain")}
51
+ onAction={() => void panelApps.refetch()}
52
+ />
53
+ );
54
+ }
55
+
56
+ if (!entry) {
57
+ return (
58
+ <PanelAppMainStatus
59
+ message={t("panelAppsMainUnavailable")}
60
+ actionLabel={t("panelAppsOpenApps")}
61
+ onAction={() => openApps(docBrowser)}
62
+ />
63
+ );
64
+ }
65
+
66
+ if (runtime.state === "checking" || runtime.state === "idle") {
67
+ return <PanelAppMainStatus message={t("panelAppsCheckingPermission")} />;
68
+ }
69
+
70
+ if (runtime.state === "denied" || runtime.state === "error") {
71
+ return (
72
+ <PanelAppMainStatus
73
+ message={runtime.state === "denied"
74
+ ? t("panelAppsPermissionDenied")
75
+ : t("panelAppsPermissionFailed")}
76
+ actionLabel={t("panelAppsTryAgain")}
77
+ onAction={() => void runtime.retryClientGrant()}
78
+ />
79
+ );
80
+ }
81
+
82
+ return (
83
+ <div className="h-full min-h-0 bg-background">
84
+ <iframe
85
+ key={entry.appId}
86
+ ref={iframeRef}
87
+ src={entry.contentPath}
88
+ title={entry.title}
89
+ sandbox={PANEL_APP_IFRAME_SANDBOX}
90
+ className="block h-full w-full border-0 bg-background"
91
+ onPointerOver={(event) => focusPanelAppIframe(event.currentTarget)}
92
+ />
93
+ </div>
94
+ );
95
+ }
96
+
97
+ function PanelAppMainStatus({
98
+ actionLabel,
99
+ message,
100
+ onAction,
101
+ }: {
102
+ actionLabel?: string;
103
+ message: string;
104
+ onAction?: () => void;
105
+ }) {
106
+ return (
107
+ <div className="flex h-full min-h-0 items-center justify-center bg-background px-6 text-center">
108
+ <div className="max-w-sm">
109
+ <div className="mx-auto flex h-10 w-10 items-center justify-center rounded-lg bg-muted text-muted-foreground">
110
+ {onAction ? <RotateCcw className="h-5 w-5" /> : <Boxes className="h-5 w-5" />}
111
+ </div>
112
+ <p className="mt-3 text-sm leading-6 text-muted-foreground">{message}</p>
113
+ {actionLabel && onAction ? (
114
+ <button
115
+ type="button"
116
+ className="mt-4 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-[var(--interaction-hover)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
117
+ onClick={onAction}
118
+ >
119
+ {actionLabel}
120
+ </button>
121
+ ) : null}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { PanelAppEntryView } from '@/shared/lib/api';
3
- import { getPanelAppViewEntries } from '../panel-app-view.utils';
3
+ import { getPanelAppViewEntries } from '@/features/panel-apps/utils/panel-app-view.utils';
4
4
 
5
5
  function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
6
6
  const createdAt = overrides.createdAt ?? '2026-05-01T00:00:00.000Z';
@@ -16,6 +16,7 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
16
16
  updatedAt,
17
17
  sizeBytes: overrides.sizeBytes ?? 12,
18
18
  favorite: overrides.favorite ?? false,
19
+ mainSidebar: overrides.mainSidebar ?? false,
19
20
  clientDeclared: overrides.clientDeclared ?? false,
20
21
  clientGranted: overrides.clientGranted ?? false,
21
22
  openCount: overrides.openCount ?? 0,
@@ -3,7 +3,7 @@ import type { DocBrowserContextValue } from '@/shared/components/doc-browser/doc
3
3
  import type { DocBrowserCustomTabRenderers } from '@/shared/components/doc-browser/doc-browser-renderer.types';
4
4
  import { getPresenter } from '@/app/presenters/app.presenter';
5
5
  import { AppsPanel, type AppsPanelTab } from '@/features/apps';
6
- import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
6
+ import { PanelAppDocBrowserToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
7
7
  import {
8
8
  createPanelAppRightPanelResourceTarget,
9
9
  RIGHT_PANEL_APPS_TAB_KIND,
@@ -75,11 +75,12 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
75
75
  onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
76
76
  onIframePointerOver: (event) => focusPanelAppIframe(event.currentTarget),
77
77
  renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
78
- renderToolbar: ({ open, refreshIframe, tab }) => (
79
- <PanelAppToolbar
78
+ renderToolbar: ({ refreshIframe, tab }) => (
79
+ <PanelAppDocBrowserToolbar
80
80
  appTitle={tab.title || t('panelAppsTitle')}
81
- onOpenApps={() => openApps({ open })}
81
+ currentUrl={tab.currentUrl}
82
82
  onRefresh={refreshIframe}
83
+ resourceUri={tab.resourceUri}
83
84
  />
84
85
  ),
85
86
  supportsScrollRestoration: true,
@@ -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,8 @@ export {
10
10
  createPanelAppContentPath,
11
11
  createPanelAppResourceUri,
12
12
  createPanelAppRightPanelResourceTarget,
13
+ readPanelAppIdFromResourceUri,
14
+ readPanelAppIdFromTab,
13
15
  RIGHT_PANEL_APPS_TAB_KIND,
14
16
  RIGHT_PANEL_APPS_URL,
15
17
  RIGHT_PANEL_HOME_TAB_KIND,
@@ -5,6 +5,8 @@ import {
5
5
  createPanelAppContentPath,
6
6
  createPanelAppResourceUri,
7
7
  createPanelAppRightPanelResourceTarget,
8
+ readPanelAppIdFromResourceUri,
9
+ readPanelAppIdFromTab,
8
10
  } from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
9
11
 
10
12
  function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
@@ -15,6 +17,7 @@ function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelA
15
17
  contentPath: '/api/panel-apps/demo/content',
16
18
  createdAt: '2026-06-02T00:00:00.000Z',
17
19
  favorite: false,
20
+ mainSidebar: false,
18
21
  fileName: 'demo.html',
19
22
  id: 'demo',
20
23
  kind: 'single-file',
@@ -69,6 +72,24 @@ describe('right-panel-resource uri utils', () => {
69
72
  );
70
73
  });
71
74
 
75
+ it('reads stable panel app ids from resource and content URLs', () => {
76
+ expect(readPanelAppIdFromResourceUri('nextclaw://panel-app/publisher.todo')).toBe('publisher.todo');
77
+ expect(readPanelAppIdFromResourceUri('/api/panel-apps/publisher.todo/content')).toBe('publisher.todo');
78
+ expect(readPanelAppIdFromResourceUri('nextclaw://panel-app/publisher.todo?path=%2Ftmp%2Ftodo.panel')).toBeNull();
79
+ expect(readPanelAppIdFromResourceUri('nextclaw://apps')).toBeNull();
80
+ });
81
+
82
+ it('falls back to the stable current URL for restored panel app shortcuts', () => {
83
+ expect(readPanelAppIdFromTab({
84
+ currentUrl: '/api/panel-apps/publisher.todo/content',
85
+ resourceUri: 'nextclaw://panel-app',
86
+ })).toBe('publisher.todo');
87
+ expect(readPanelAppIdFromTab({
88
+ currentUrl: '/api/panel-apps/publisher.todo/content?path=%2Ftmp%2Ftodo.panel',
89
+ resourceUri: 'nextclaw://panel-app/publisher.todo?path=%2Ftmp%2Ftodo.panel',
90
+ })).toBeNull();
91
+ });
92
+
72
93
  it('keeps panel app image icons as dock image icons', () => {
73
94
  const target = createPanelAppRightPanelResourceTarget(createPanelAppEntry({
74
95
  icon: '/api/panel-apps/demo/assets/icon.png',
@@ -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,42 @@ 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
+
110
+ export function readPanelAppIdFromTab(
111
+ tab: Pick<DocBrowserTab, 'currentUrl' | 'resourceUri'>,
112
+ ): string | null {
113
+ return (tab.resourceUri
114
+ ? readPanelAppIdFromResourceUri(tab.resourceUri)
115
+ : null) ?? readPanelAppIdFromResourceUri(tab.currentUrl);
116
+ }
117
+
81
118
  function isPanelAppImageIcon(icon: string): boolean {
82
119
  return (
83
120
  icon.startsWith('data:image/') ||
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,76 @@ 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
+ it("keeps placement actions when a restored shortcut only has a stable current URL", async () => {
542
+ const user = userEvent.setup();
543
+ const panelAppTab: DocBrowserTab = {
544
+ id: "piano",
545
+ kind: "panel-app",
546
+ title: "Piano",
547
+ currentUrl: "/api/panel-apps/piano/content",
548
+ resourceUri: "nextclaw://panel-app",
549
+ history: ["/api/panel-apps/piano/content"],
550
+ historyIndex: 0,
551
+ navVersion: 0,
552
+ };
553
+ docBrowserState.tabs = [panelAppTab];
554
+ docBrowserState.activeTabId = panelAppTab.id;
555
+ docBrowserState.currentTab = panelAppTab;
556
+
557
+ render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
558
+
559
+ await user.click(screen.getByRole("button", { name: "More panel app actions" }));
560
+ await user.click(screen.getByRole("button", { name: "Add to main sidebar" }));
561
+ expect(panelAppHooks.mutate).toHaveBeenCalledWith({
562
+ id: "piano",
563
+ preferences: { mainSidebar: true },
564
+ });
565
+ });
566
+ });
567
+
466
568
  describe("DocBrowser scroll restoration", () => {
467
569
  beforeEach(() => {
468
570
  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": "将删除这个面板应用文件,删除后不可恢复。",