@nextclaw/ui 0.12.30 → 0.12.32

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 (66) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/assets/{api-CKKNxbsy.js → api-rASx2-N4.js} +1 -1
  3. package/dist/assets/{app-manager-provider-BM1hziPv.js → app-manager-provider-CUkcbdWk.js} +1 -1
  4. package/dist/assets/{channels-list-page-B6JASU3q.js → channels-list-page-CE_9jne_.js} +1 -1
  5. package/dist/assets/{chat-page-C_lQm75P.js → chat-page-BMJLtk3O.js} +1 -1
  6. package/dist/assets/cpu-DKc3bQic.js +3 -0
  7. package/dist/assets/desktop-DJpQzxsz.js +3 -0
  8. package/dist/assets/{desktop-update-config-jWt63bXG.js → desktop-update-config-DAD9JN9Q.js} +1 -1
  9. package/dist/assets/{dialog-DWZeMf9V.js → dialog-CR80q3Rm.js} +1 -1
  10. package/dist/assets/{dist-DN-YM_0Y.js → dist-NBjcwlsw.js} +1 -1
  11. package/dist/assets/doc-browser-B19cYGxa.js +1 -0
  12. package/dist/assets/{doc-browser-CAhfnm0D.js → doc-browser-CDBYvb8A.js} +1 -1
  13. package/dist/assets/doc-browser-IRh6LCzu.js +1 -0
  14. package/dist/assets/{doc-browser-context-FukQHvyo.js → doc-browser-context-BPeeiNJ_.js} +1 -1
  15. package/dist/assets/{es2015-ZJ-nWL4L.js → es2015-CVAuMqlM.js} +1 -1
  16. package/dist/assets/index-BEn7o-oH.js +100 -0
  17. package/dist/assets/index-DV57tBmg.css +1 -0
  18. package/dist/assets/{marketplace-page-BdiLk2eA.js → marketplace-page-B3Q_uKOZ.js} +2 -2
  19. package/dist/assets/marketplace-page-CCq1bFb1.js +1 -0
  20. package/dist/assets/{mcp-marketplace-page-DTCFgcDl.js → mcp-marketplace-page-D9maE_Sf.js} +1 -1
  21. package/dist/assets/mcp-marketplace-page-O5AQzBYl.js +1 -0
  22. package/dist/assets/{model-config-QChwQThC.js → model-config-BsEQYJvz.js} +1 -1
  23. package/dist/assets/{notice-card-BM2_YNoc.js → notice-card-CF4AJPM3.js} +1 -1
  24. package/dist/assets/{popover-2H9DS7Cv.js → popover-DeAliE_4.js} +1 -1
  25. package/dist/assets/{provider-scoped-model-input-DsZJ44ZV.js → provider-scoped-model-input-B8LJ6RCt.js} +1 -1
  26. package/dist/assets/{providers-list-lpGJJ21M.js → providers-list-5mA-2zno.js} +1 -1
  27. package/dist/assets/remote-DiPWGRZc.js +1 -0
  28. package/dist/assets/runtime-config-page-BSoKMvCh.js +1 -0
  29. package/dist/assets/{search-config-CJOrq3h0.js → search-config-DxWm2GrK.js} +1 -1
  30. package/dist/assets/{secrets-config-C17j6mjS.js → secrets-config-qutTAlFx.js} +1 -1
  31. package/dist/assets/{select-C2c10F3e.js → select-HI5kiwX_.js} +1 -1
  32. package/dist/assets/{setting-row-CB_ZFbIZ.js → setting-row-B4QnghjV.js} +1 -1
  33. package/dist/assets/{tag-chip-CrQfVN3A.js → tag-chip-BPgMfp9l.js} +1 -1
  34. package/dist/assets/{use-config-jAYudJxy.js → use-config-BFqzVF8Y.js} +1 -1
  35. package/dist/assets/{use-infinite-scroll-loader-zzviKbU8.js → use-infinite-scroll-loader-CyNa4Vj6.js} +1 -1
  36. package/dist/index.html +18 -18
  37. package/package.json +9 -9
  38. package/src/app/components/layout/app-layout.test.tsx +58 -4
  39. package/src/app/components/layout/app-layout.tsx +2 -2
  40. package/src/app/components/theme-provider.tsx +0 -1
  41. package/src/app/hooks/use-viewport-layout.ts +0 -1
  42. package/src/features/chat/components/chat-session-workspace-panel.tsx +54 -51
  43. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +2 -1
  44. package/src/features/chat/managers/chat-session-list.manager.test.ts +6 -4
  45. package/src/features/chat/managers/chat-session-list.manager.ts +0 -6
  46. package/src/features/chat/utils/chat-message-invalid-tool-arguments.utils.test.ts +79 -0
  47. package/src/features/chat/utils/chat-message-part.utils.ts +34 -19
  48. package/src/main.tsx +5 -3
  49. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +16 -4
  50. package/src/platforms/desktop/components/desktop-window-chrome.tsx +56 -13
  51. package/src/platforms/desktop/types/desktop-update.types.ts +2 -2
  52. package/src/shared/components/doc-browser/doc-browser-context.tsx +0 -27
  53. package/src/shared/components/doc-browser/doc-browser.test.tsx +11 -9
  54. package/src/shared/components/doc-browser/doc-browser.tsx +37 -63
  55. package/src/shared/components/resizable-right-panel/resizable-right-panel.test.tsx +38 -0
  56. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +46 -0
  57. package/dist/assets/cpu-DPPwMzoC.js +0 -3
  58. package/dist/assets/desktop-CmIbyIrg.js +0 -3
  59. package/dist/assets/doc-browser-p9DDNPWB.js +0 -1
  60. package/dist/assets/doc-browser-rZIQIjuw.js +0 -1
  61. package/dist/assets/index-CTfzteME.js +0 -100
  62. package/dist/assets/index-DKC9hvsP.css +0 -1
  63. package/dist/assets/marketplace-page-C3RVIckE.js +0 -1
  64. package/dist/assets/mcp-marketplace-page-BVCDibC7.js +0 -1
  65. package/dist/assets/remote-Dm6hKGew.js +0 -1
  66. package/dist/assets/runtime-config-page-CDEAJFfY.js +0 -1
package/dist/index.html CHANGED
@@ -78,35 +78,35 @@
78
78
  })();
79
79
  </script>
80
80
  <title>NextClaw</title>
81
- <script type="module" crossorigin src="/assets/index-CTfzteME.js"></script>
81
+ <script type="module" crossorigin src="/assets/index-BEn7o-oH.js"></script>
82
82
  <link rel="modulepreload" crossorigin href="/assets/i18n-D1144VAA.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DzY6wN61.js">
84
- <link rel="modulepreload" crossorigin href="/assets/cpu-DPPwMzoC.js">
85
- <link rel="modulepreload" crossorigin href="/assets/api-CKKNxbsy.js">
86
- <link rel="modulepreload" crossorigin href="/assets/es2015-ZJ-nWL4L.js">
87
- <link rel="modulepreload" crossorigin href="/assets/select-C2c10F3e.js">
88
- <link rel="modulepreload" crossorigin href="/assets/dist-DN-YM_0Y.js">
84
+ <link rel="modulepreload" crossorigin href="/assets/cpu-DKc3bQic.js">
85
+ <link rel="modulepreload" crossorigin href="/assets/api-rASx2-N4.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/es2015-CVAuMqlM.js">
87
+ <link rel="modulepreload" crossorigin href="/assets/select-HI5kiwX_.js">
88
+ <link rel="modulepreload" crossorigin href="/assets/dist-NBjcwlsw.js">
89
89
  <link rel="modulepreload" crossorigin href="/assets/x-BeyYA_h6.js">
90
- <link rel="modulepreload" crossorigin href="/assets/dialog-DWZeMf9V.js">
91
- <link rel="modulepreload" crossorigin href="/assets/popover-2H9DS7Cv.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/dialog-CR80q3Rm.js">
91
+ <link rel="modulepreload" crossorigin href="/assets/popover-DeAliE_4.js">
92
92
  <link rel="modulepreload" crossorigin href="/assets/search-CRtQwr-h.js">
93
93
  <link rel="modulepreload" crossorigin href="/assets/book-open-BcnAiKde.js">
94
+ <link rel="modulepreload" crossorigin href="/assets/external-link-CpEvG65F.js">
95
+ <link rel="modulepreload" crossorigin href="/assets/plus-B-YHtTNC.js">
96
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-BPeeiNJ_.js">
97
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-B19cYGxa.js">
94
98
  <link rel="modulepreload" crossorigin href="/assets/download-CMM8po31.js">
95
99
  <link rel="modulepreload" crossorigin href="/assets/key-round-DUq47t0P.js">
96
- <link rel="modulepreload" crossorigin href="/assets/notice-card-BM2_YNoc.js">
100
+ <link rel="modulepreload" crossorigin href="/assets/notice-card-CF4AJPM3.js">
97
101
  <link rel="modulepreload" crossorigin href="/assets/status-dot-l3kPFdq_.js">
98
- <link rel="modulepreload" crossorigin href="/assets/app-manager-provider-BM1hziPv.js">
102
+ <link rel="modulepreload" crossorigin href="/assets/app-manager-provider-CUkcbdWk.js">
99
103
  <link rel="modulepreload" crossorigin href="/assets/settings-MWL2SMyk.js">
100
104
  <link rel="modulepreload" crossorigin href="/assets/refresh-cw-CrbD8EkT.js">
101
105
  <link rel="modulepreload" crossorigin href="/assets/sparkles-BmgOD4nY.js">
102
- <link rel="modulepreload" crossorigin href="/assets/external-link-CpEvG65F.js">
103
- <link rel="modulepreload" crossorigin href="/assets/plus-B-YHtTNC.js">
104
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-FukQHvyo.js">
105
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-rZIQIjuw.js">
106
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-CAhfnm0D.js">
107
- <link rel="modulepreload" crossorigin href="/assets/use-config-jAYudJxy.js">
108
- <link rel="modulepreload" crossorigin href="/assets/desktop-CmIbyIrg.js">
109
- <link rel="stylesheet" crossorigin href="/assets/index-DKC9hvsP.css">
106
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-CDBYvb8A.js">
107
+ <link rel="modulepreload" crossorigin href="/assets/use-config-BFqzVF8Y.js">
108
+ <link rel="modulepreload" crossorigin href="/assets/desktop-DJpQzxsz.js">
109
+ <link rel="stylesheet" crossorigin href="/assets/index-DV57tBmg.css">
110
110
  </head>
111
111
 
112
112
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.12.30",
3
+ "version": "0.12.32",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,14 +28,14 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/agent-chat": "0.1.18",
32
- "@nextclaw/client-sdk": "0.1.9",
33
- "@nextclaw/agent-chat-ui": "0.3.20",
34
- "@nextclaw/ncp": "0.5.13",
35
- "@nextclaw/ncp-http-agent-client": "0.3.25",
36
- "@nextclaw/server": "0.12.21",
37
- "@nextclaw/ncp-react": "0.4.33",
38
- "@nextclaw/shared": "0.1.7"
31
+ "@nextclaw/agent-chat": "0.1.20",
32
+ "@nextclaw/agent-chat-ui": "0.3.22",
33
+ "@nextclaw/client-sdk": "0.1.11",
34
+ "@nextclaw/ncp": "0.5.15",
35
+ "@nextclaw/ncp-http-agent-client": "0.3.27",
36
+ "@nextclaw/ncp-react": "0.4.35",
37
+ "@nextclaw/server": "0.12.23",
38
+ "@nextclaw/shared": "0.1.9"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@testing-library/react": "^16.3.0",
@@ -1,6 +1,8 @@
1
1
  import { render, screen } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import type { ReactNode } from "react";
2
4
  import { MemoryRouter, Route, Routes } from "react-router-dom";
3
- import { describe, expect, it, vi } from "vitest";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
6
  import { AppLayout } from "@/app/components/layout/app-layout";
5
7
  import { I18nProvider } from "@/app/components/i18n-provider";
6
8
 
@@ -22,7 +24,22 @@ vi.mock("@/app/components/layout/sidebar", () => ({
22
24
  ),
23
25
  }));
24
26
 
27
+ vi.mock("@/platforms/mobile", () => ({
28
+ MobileAppShell: ({ children }: { children: ReactNode }) => (
29
+ <div data-testid="mobile-app-shell">{children}</div>
30
+ ),
31
+ }));
32
+
25
33
  describe("AppLayout", () => {
34
+ beforeEach(() => {
35
+ useViewportLayoutMock.mockReturnValue({
36
+ mode: "desktop",
37
+ isMobile: false,
38
+ isDesktop: true,
39
+ });
40
+ window.nextclawDesktop = undefined;
41
+ });
42
+
26
43
  it("treats /agents as a main workspace route instead of the settings shell", () => {
27
44
  const { container } = render(
28
45
  <I18nProvider>
@@ -97,13 +114,50 @@ describe("AppLayout", () => {
97
114
  );
98
115
 
99
116
  expect(screen.getByTestId("chat-content")).toBeTruthy();
117
+ expect(screen.getByTestId("mobile-app-shell")).toBeTruthy();
100
118
  expect(screen.queryByTestId("settings-sidebar-header")).toBeNull();
101
119
  expect(container.querySelector("aside")).toBeNull();
120
+ });
102
121
 
122
+ it("keeps the desktop shell on desktop hosts even when the viewport is narrow", () => {
123
+ window.nextclawDesktop = {
124
+ platform: "win32",
125
+ } as typeof window.nextclawDesktop;
103
126
  useViewportLayoutMock.mockReturnValue({
104
- mode: "desktop",
105
- isMobile: false,
106
- isDesktop: true,
127
+ mode: "mobile",
128
+ isMobile: true,
129
+ isDesktop: false,
107
130
  });
131
+
132
+ const queryClient = new QueryClient({
133
+ defaultOptions: {
134
+ queries: {
135
+ retry: false,
136
+ },
137
+ },
138
+ });
139
+
140
+ render(
141
+ <QueryClientProvider client={queryClient}>
142
+ <I18nProvider>
143
+ <MemoryRouter initialEntries={["/chat"]}>
144
+ <Routes>
145
+ <Route
146
+ path="*"
147
+ element={
148
+ <AppLayout>
149
+ <div data-testid="chat-content">Chat Content</div>
150
+ </AppLayout>
151
+ }
152
+ />
153
+ </Routes>
154
+ </MemoryRouter>
155
+ </I18nProvider>
156
+ </QueryClientProvider>,
157
+ );
158
+
159
+ expect(screen.getByTestId("chat-content")).toBeTruthy();
160
+ expect(screen.getByTestId("desktop-window-chrome")).toBeTruthy();
161
+ expect(screen.getByTestId("desktop-window-chrome-main-drag-region")).toBeTruthy();
108
162
  });
109
163
  });
@@ -7,7 +7,7 @@ import {
7
7
  import { useDocLinkInterceptor } from "@/shared/components/doc-browser/use-doc-link-interceptor";
8
8
  import { useI18n } from "@/app/components/i18n-provider";
9
9
  import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
10
- import { DesktopAppShell } from "@/platforms/desktop";
10
+ import { DesktopAppShell, getDesktopHostPlatform } from "@/platforms/desktop";
11
11
  import { MobileAppShell } from "@/platforms/mobile";
12
12
  import { resolveUiDocumentTitle } from "@/shared/lib/ui-document-title";
13
13
 
@@ -26,7 +26,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
26
26
  document.title = resolveUiDocumentTitle(pathname);
27
27
  }, [pathname, language]);
28
28
 
29
- if (isMobile) {
29
+ if (isMobile && getDesktopHostPlatform() === null) {
30
30
  return (
31
31
  <MobileAppShell pathname={pathname} isDocBrowserOpen={isOpen}>
32
32
  {children}
@@ -28,7 +28,6 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
28
28
 
29
29
  useEffect(() => {
30
30
  pwaShellThemeManager.syncTheme(theme);
31
- void window.nextclawDesktop?.setShellTheme?.(theme);
32
31
  }, [theme]);
33
32
 
34
33
  useEffect(() => {
@@ -10,6 +10,5 @@ export function useViewportLayout() {
10
10
  return {
11
11
  mode,
12
12
  isMobile: mode === "mobile",
13
- isDesktop: mode === "desktop",
14
13
  };
15
14
  }
@@ -30,6 +30,7 @@ import { usePresenter } from "@/features/chat/components/providers/chat-presente
30
30
  import { ChatSessionWorkspaceFilePreview } from "./chat-session-workspace-file-preview";
31
31
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
32
32
  import { SessionCronJobContent } from "@/features/chat/components/workspace/session-cron-job-content";
33
+ import { ResizableRightPanel } from "@/shared/components/resizable-right-panel/resizable-right-panel";
33
34
  import { t } from "@/shared/lib/i18n";
34
35
  import { cn } from "@/shared/lib/utils";
35
36
 
@@ -314,63 +315,65 @@ export function ChatSessionWorkspacePanel({
314
315
  }
315
316
 
316
317
  return (
317
- <aside
318
+ <ResizableRightPanel
318
319
  className={cn(
319
320
  displayMode === "overlay"
320
- ? "fixed inset-0 z-40 flex bg-white"
321
- : "hidden shrink-0 border-l border-gray-200/70 bg-white/95 backdrop-blur-sm md:flex md:w-[26rem] lg:w-[30rem] xl:w-[34rem]",
321
+ ? "bg-white"
322
+ : "hidden border-gray-200/70 bg-white/95 backdrop-blur-sm md:flex",
322
323
  )}
324
+ defaultWidth={480}
325
+ minWidth={360}
326
+ maxWidth={860}
327
+ overlay={displayMode === "overlay"}
323
328
  >
324
- <div className="flex h-full min-h-0 w-full flex-col">
325
- <div className="flex items-center justify-between gap-3 border-b border-gray-200/70 px-4 py-2.5">
326
- <button
327
- type="button"
328
- onClick={presenter.chatThreadManager.goToParentSession}
329
- className={cn(
330
- "inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
331
- !hasParentSession && "pointer-events-none opacity-0",
332
- )}
333
- >
334
- <ArrowLeft className="h-3.5 w-3.5" />
335
- <span>{t("chatBackToParent")}</span>
336
- </button>
337
- <button
338
- type="button"
339
- onClick={presenter.chatThreadManager.closeWorkspacePanel}
340
- className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
341
- aria-label={t("chatWorkspaceClosePanel")}
342
- >
343
- <X className="h-4 w-4" />
344
- </button>
345
- </div>
329
+ <div className="flex items-center justify-between gap-3 border-b border-gray-200/70 px-4 py-2.5">
330
+ <button
331
+ type="button"
332
+ onClick={presenter.chatThreadManager.goToParentSession}
333
+ className={cn(
334
+ "inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
335
+ !hasParentSession && "pointer-events-none opacity-0",
336
+ )}
337
+ >
338
+ <ArrowLeft className="h-3.5 w-3.5" />
339
+ <span>{t("chatBackToParent")}</span>
340
+ </button>
341
+ <button
342
+ type="button"
343
+ onClick={presenter.chatThreadManager.closeWorkspacePanel}
344
+ className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
345
+ aria-label={t("chatWorkspaceClosePanel")}
346
+ >
347
+ <X className="h-4 w-4" />
348
+ </button>
349
+ </div>
346
350
 
347
- <WorkspaceTabsBar tabs={workspaceTabs} />
351
+ <WorkspaceTabsBar tabs={workspaceTabs} />
348
352
 
349
- <div className="flex min-h-0 flex-1 flex-col bg-white">
350
- {activeSelection.kind === "child-session" ? (
351
- <>
352
- <WorkspaceActiveChildHeader tab={activeSelection.tab} />
353
- <div className="flex-1 min-h-0">
354
- <ChildSessionContent
355
- sessionKey={activeSelection.tab.sessionKey}
356
- onToolAction={
357
- presenter.chatThreadManager.openSessionFromToolAction
358
- }
359
- onFileOpen={presenter.chatThreadManager.openFilePreview}
360
- />
361
- </div>
362
- </>
363
- ) : activeSelection.kind === "file" ? (
364
- <ChatSessionWorkspaceFilePreview
365
- file={activeSelection.file}
366
- sessionProjectRoot={sessionProjectRoot}
367
- onFileOpen={presenter.chatThreadManager.openFilePreview}
368
- />
369
- ) : (
370
- <SessionCronJobContent jobs={sessionCronJobs} />
371
- )}
372
- </div>
353
+ <div className="flex min-h-0 flex-1 flex-col bg-white">
354
+ {activeSelection.kind === "child-session" ? (
355
+ <>
356
+ <WorkspaceActiveChildHeader tab={activeSelection.tab} />
357
+ <div className="flex-1 min-h-0">
358
+ <ChildSessionContent
359
+ sessionKey={activeSelection.tab.sessionKey}
360
+ onToolAction={
361
+ presenter.chatThreadManager.openSessionFromToolAction
362
+ }
363
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
364
+ />
365
+ </div>
366
+ </>
367
+ ) : activeSelection.kind === "file" ? (
368
+ <ChatSessionWorkspaceFilePreview
369
+ file={activeSelection.file}
370
+ sessionProjectRoot={sessionProjectRoot}
371
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
372
+ />
373
+ ) : (
374
+ <SessionCronJobContent jobs={sessionCronJobs} />
375
+ )}
373
376
  </div>
374
- </aside>
377
+ </ResizableRightPanel>
375
378
  );
376
379
  }
@@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { ChatConversationPanel } from "@/features/chat/components/conversation/chat-conversation-panel";
5
- import { ChatSessionWorkspacePanel } from "@/features/chat";
5
+ import { ChatSessionWorkspacePanel } from "@/features/chat/components/chat-session-workspace-panel";
6
6
  import type { ResolvedChildSessionTab } from "@/features/chat/hooks/use-ncp-child-session-tabs-view";
7
7
  import type { CronJobView } from "@/shared/lib/api";
8
8
  import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
@@ -556,6 +556,7 @@ describe("ChatSessionWorkspacePanel", () => {
556
556
  );
557
557
 
558
558
  expect(screen.queryByText("Child sessions")).toBeNull();
559
+ expect(screen.getByTestId("resizable-right-panel-handle")).toBeTruthy();
559
560
  expect(screen.getAllByText("北京天气")).toHaveLength(2);
560
561
  expect(screen.getByText("Codex")).toBeTruthy();
561
562
  expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
@@ -42,8 +42,9 @@ describe('ChatSessionListManager', () => {
42
42
  snapshot: {
43
43
  ...useChatThreadStore.getState().snapshot,
44
44
  workspacePanelParentKey: 'session-1',
45
+ activeWorkspacePanelKind: 'child-session',
45
46
  activeChildSessionKey: 'child-session-1',
46
- activeWorkspaceFileKey: 'session-1::/tmp/demo.md',
47
+ activeWorkspaceFileKey: null,
47
48
  },
48
49
  });
49
50
  });
@@ -155,7 +156,7 @@ describe('ChatSessionListManager', () => {
155
156
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
156
157
  });
157
158
 
158
- it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
159
+ it('delegates existing-session selection to routing while preserving workspace panel state', () => {
159
160
  const uiManager = {
160
161
  goToChatRoot: vi.fn(),
161
162
  goToSession: vi.fn(),
@@ -170,8 +171,9 @@ describe('ChatSessionListManager', () => {
170
171
 
171
172
  expect(uiManager.goToSession).toHaveBeenCalledWith('session-2');
172
173
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
173
- expect(useChatThreadStore.getState().snapshot.workspacePanelParentKey).toBeNull();
174
- expect(useChatThreadStore.getState().snapshot.activeChildSessionKey).toBeNull();
174
+ expect(useChatThreadStore.getState().snapshot.workspacePanelParentKey).toBe('session-1');
175
+ expect(useChatThreadStore.getState().snapshot.activeWorkspacePanelKind).toBe('child-session');
176
+ expect(useChatThreadStore.getState().snapshot.activeChildSessionKey).toBe('child-session-1');
175
177
  expect(useChatThreadStore.getState().snapshot.activeWorkspaceFileKey).toBeNull();
176
178
  });
177
179
 
@@ -154,12 +154,6 @@ export class ChatSessionListManager {
154
154
  };
155
155
 
156
156
  selectSession = (sessionKey: string) => {
157
- useChatThreadStore.getState().setSnapshot({
158
- workspacePanelParentKey: null,
159
- activeWorkspacePanelKind: null,
160
- activeChildSessionKey: null,
161
- activeWorkspaceFileKey: null,
162
- });
163
157
  this.uiManager.goToSession(sessionKey);
164
158
  };
165
159
 
@@ -0,0 +1,79 @@
1
+ import { ToolInvocationStatus, type UiMessage } from "@nextclaw/agent-chat";
2
+ import { adaptChatMessages } from "./chat-message.utils";
3
+ import type { ChatMessageSource } from "./chat-message.utils";
4
+
5
+ const defaultTexts = {
6
+ roleLabels: {
7
+ user: "You",
8
+ assistant: "Assistant",
9
+ tool: "Tool",
10
+ system: "System",
11
+ fallback: "Message",
12
+ },
13
+ reasoningLabel: "Reasoning",
14
+ toolCallLabel: "Tool Call",
15
+ toolResultLabel: "Tool Result",
16
+ toolInputLabel: "Input",
17
+ toolNoOutputLabel: "No output",
18
+ toolOutputLabel: "Output",
19
+ toolStatusPreparingLabel: "Preparing",
20
+ toolStatusRunningLabel: "Running",
21
+ toolStatusCompletedLabel: "Completed",
22
+ toolStatusFailedLabel: "Failed",
23
+ toolStatusCancelledLabel: "Cancelled",
24
+ imageAttachmentLabel: "Image attachment",
25
+ fileAttachmentLabel: "File attachment",
26
+ unknownPartLabel: "Unknown Part",
27
+ };
28
+
29
+ function adapt(uiMessages: UiMessage[]) {
30
+ return adaptChatMessages({
31
+ uiMessages: uiMessages as unknown as ChatMessageSource[],
32
+ formatTimestamp: (value) => `formatted:${value}`,
33
+ texts: defaultTexts,
34
+ });
35
+ }
36
+
37
+ it("renders invalid tool arguments as a failed argument card instead of a normal raw-args card", () => {
38
+ const rawArgumentsText = '{"task":"spawn","request">\n<||DSML||parameter: "final_reply"}';
39
+ const adapted = adapt([
40
+ {
41
+ id: "assistant-invalid-tool-args",
42
+ role: "assistant",
43
+ parts: [
44
+ {
45
+ type: "tool-invocation",
46
+ toolInvocation: {
47
+ status: ToolInvocationStatus.RESULT,
48
+ toolCallId: "call-invalid-args",
49
+ toolName: "sessions_spawn",
50
+ args: rawArgumentsText,
51
+ result: {
52
+ ok: false,
53
+ error: {
54
+ code: "invalid_tool_arguments",
55
+ message: "Tool arguments are invalid.",
56
+ toolCallId: "call-invalid-args",
57
+ toolName: "sessions_spawn",
58
+ rawArgumentsText,
59
+ issues: ["Invalid JSON"],
60
+ },
61
+ },
62
+ },
63
+ },
64
+ ],
65
+ },
66
+ ]);
67
+
68
+ expect(adapted[0]?.parts[0]).toMatchObject({
69
+ type: "tool-card",
70
+ card: {
71
+ toolName: "sessions_spawn",
72
+ summary: "Invalid arguments: Invalid JSON",
73
+ input: rawArgumentsText,
74
+ output: "Invalid JSON",
75
+ statusTone: "error",
76
+ statusLabel: "Failed",
77
+ },
78
+ });
79
+ });
@@ -21,7 +21,6 @@ import { buildFileOperationCardData } from "./file-operation/card.utils";
21
21
  import { buildSessionRequestToolCard } from "./chat-message-session-request-tool-card.utils";
22
22
  import type {
23
23
  ChatMessagePartViewModel,
24
- ChatToolPartViewModel,
25
24
  } from "@nextclaw/agent-chat-ui";
26
25
  import type {
27
26
  ChatMessageAdapterTexts,
@@ -39,6 +38,17 @@ type ChatMessagePartAdapterParams = {
39
38
  texts: ChatMessageAdapterTexts;
40
39
  };
41
40
 
41
+ function readInvalidToolArgumentsError(value: unknown): Record<string, unknown> | null {
42
+ const error = typeof value === "object" && value !== null && !Array.isArray(value)
43
+ ? (value as { error?: unknown }).error
44
+ : null;
45
+ return typeof error === "object" &&
46
+ error !== null &&
47
+ !Array.isArray(error) &&
48
+ (error as { code?: unknown }).code === "invalid_tool_arguments"
49
+ ? error as Record<string, unknown>
50
+ : null;
51
+ }
42
52
 
43
53
  function buildReasoningPart(
44
54
  part: Extract<ChatMessagePartSource, { type: "reasoning" }>,
@@ -113,6 +123,10 @@ function buildToolInvocationPart(
113
123
  result: invocation.result,
114
124
  texts,
115
125
  });
126
+ const invalidArgsError = readInvalidToolArgumentsError(invocation.result);
127
+ const invalidArgsIssue = Array.isArray(invalidArgsError?.issues)
128
+ ? invalidArgsError.issues.find((issue): issue is string => typeof issue === "string" && issue.trim().length > 0)
129
+ : undefined;
116
130
  const fileOperationCardData = buildFileOperationCardData({
117
131
  toolName: invocation.toolName,
118
132
  status: invocation.status,
@@ -121,14 +135,14 @@ function buildToolInvocationPart(
121
135
  parsedArgs: invocation.parsedArgs,
122
136
  result: invocation.result,
123
137
  });
124
- const detail =
125
- fileOperationCardData?.summary ??
126
- summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
127
- const input = fileOperationCardData
128
- ? undefined
129
- : buildToolInvocationInput(invocation.args, invocation.parsedArgs);
138
+ const detail = invalidArgsIssue
139
+ ? `Invalid arguments: ${invalidArgsIssue}`
140
+ : fileOperationCardData?.summary ?? summarizeToolArgs(invocation.parsedArgs ?? invocation.args);
141
+ const input = fileOperationCardData ? undefined : buildToolInvocationInput(invocation.args, invocation.parsedArgs);
130
142
  const rawResult =
131
- typeof invocation.error === "string" && invocation.error.trim()
143
+ invalidArgsIssue
144
+ ? invalidArgsIssue
145
+ : typeof invocation.error === "string" && invocation.error.trim()
132
146
  ? invocation.error.trim()
133
147
  : invocation.result != null
134
148
  ? stringifyUnknown(invocation.result).trim()
@@ -149,8 +163,8 @@ function buildToolInvocationPart(
149
163
  outputData: invocation.result,
150
164
  callId: invocation.toolCallId || undefined,
151
165
  hasResult: statusView.hasResult,
152
- statusTone: statusView.statusTone,
153
- statusLabel: statusView.statusLabel,
166
+ statusTone: invalidArgsError ? "error" : statusView.statusTone,
167
+ statusLabel: invalidArgsError ? texts.toolStatusFailedLabel : statusView.statusLabel,
154
168
  ...(fileOperationCardData?.fileOperation
155
169
  ? { fileOperation: fileOperationCardData.fileOperation }
156
170
  : {}),
@@ -217,17 +231,18 @@ function isToolInvocationPart(
217
231
  export function adaptChatMessagePart(
218
232
  params: ChatMessagePartAdapterParams,
219
233
  ): ChatMessagePartViewModel | null {
220
- if (isTextPart(params.part)) {
221
- return buildTextPart(params.part, params.inlineTokens);
234
+ const { inlineTokens, part, texts } = params;
235
+ if (isTextPart(part)) {
236
+ return buildTextPart(part, inlineTokens);
222
237
  }
223
- if (isReasoningPart(params.part)) {
224
- return buildReasoningPart(params.part, params.texts);
238
+ if (isReasoningPart(part)) {
239
+ return buildReasoningPart(part, texts);
225
240
  }
226
- if (isFilePart(params.part)) {
227
- return buildFilePart(params.part, params.texts);
241
+ if (isFilePart(part)) {
242
+ return buildFilePart(part, texts);
228
243
  }
229
- if (isToolInvocationPart(params.part)) {
230
- return buildToolInvocationPart(params.part, params.texts);
244
+ if (isToolInvocationPart(part)) {
245
+ return buildToolInvocationPart(part, texts);
231
246
  }
232
- return buildUnknownPart(params.part, params.texts);
247
+ return buildUnknownPart(part, texts);
233
248
  }
package/src/main.tsx CHANGED
@@ -1,18 +1,20 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import { BrowserRouter } from 'react-router-dom';
3
+ import { BrowserRouter, MemoryRouter } from 'react-router-dom';
4
4
  import App from './app';
5
5
  import { I18nProvider } from '@/components/providers/I18nProvider';
6
6
  import { ThemeProvider } from '@/components/providers/ThemeProvider';
7
7
  import './index.css';
8
8
 
9
+ const AppRouter = window.nextclawDesktop?.platform === 'win32' ? MemoryRouter : BrowserRouter;
10
+
9
11
  createRoot(document.getElementById('root')!).render(
10
12
  <StrictMode>
11
13
  <ThemeProvider>
12
14
  <I18nProvider>
13
- <BrowserRouter>
15
+ <AppRouter>
14
16
  <App />
15
- </BrowserRouter>
17
+ </AppRouter>
16
18
  </I18nProvider>
17
19
  </ThemeProvider>
18
20
  </StrictMode>
@@ -48,14 +48,26 @@ describe("DesktopAppShell", () => {
48
48
 
49
49
  const chrome = screen.getByTestId("desktop-window-chrome");
50
50
  const sidebarChrome = screen.getByTestId("desktop-window-chrome-sidebar");
51
- const mainChrome = screen.getByTestId("desktop-window-chrome-main");
51
+ const mainDragRegion = screen.getByTestId("desktop-window-chrome-main-drag-region");
52
+ const resizeStrip = screen.getByTestId("desktop-window-chrome-resize-strip");
53
+ const controls = screen.getByTestId("desktop-window-controls");
52
54
 
53
55
  expect(chrome).toBeTruthy();
54
56
  expect(chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height")).toBe("40px");
57
+ expect(chrome.className).toContain("bg-secondary");
58
+ expect(chrome.className).toContain("border-b");
59
+ expect(chrome.className).toContain("desktop-window-drag");
55
60
  expect(sidebarChrome.className).toContain("w-[var(--desktop-sidebar-width)]");
56
- expect(sidebarChrome.className).not.toContain("border-b");
57
- expect(mainChrome.className).toContain("border-b");
58
- expect(mainChrome.className).toContain("pr-[var(--desktop-caption-safe-right)]");
61
+ expect(sidebarChrome.className).toContain("desktop-window-no-drag");
62
+ expect(mainDragRegion.className).toContain("flex-1");
63
+ expect(mainDragRegion.className).toContain("desktop-window-drag");
64
+ expect(resizeStrip.className).toContain("desktop-window-no-drag");
65
+ expect(resizeStrip.className).toContain("top-0");
66
+ expect(resizeStrip.className).toContain("h-1");
67
+ expect(controls.className).toContain("desktop-window-no-drag");
68
+ expect(screen.getByLabelText("Minimize").className).toContain("desktop-window-no-drag");
69
+ expect(screen.getByLabelText("Maximize").className).toContain("desktop-window-no-drag");
70
+ expect(screen.getByLabelText("Close").className).toContain("desktop-window-no-drag");
59
71
  expect(screen.getByTestId("app-content")).toBeTruthy();
60
72
  });
61
73