@nextclaw/ui 0.20.3 → 0.22.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 (136) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/assets/{api-iMkU_o5P.js → api-m33O1Kb0.js} +6 -6
  3. package/dist/assets/{appearance-settings-page-CU5k4b-M.js → appearance-settings-page-x4Vjnfmz.js} +1 -1
  4. package/dist/assets/{book-open-nD8gdMhA.js → book-open-BHiasMCg.js} +1 -1
  5. package/dist/assets/{channels-list-page-Drlt62kY.js → channels-list-page-DOxJZeXq.js} +2 -2
  6. package/dist/assets/{config-split-page-BtJZp1E5.js → config-split-page-DBnMEP1o.js} +1 -1
  7. package/dist/assets/{confirm-dialog-k2-Jq_x3.js → confirm-dialog-f5NKLh5I.js} +2 -2
  8. package/dist/assets/{createLucideIcon-DxlwKLr-.js → createLucideIcon-CHALGA42.js} +1 -1
  9. package/dist/assets/desktop-capabilities-page-DixayAe6.js +1 -0
  10. package/dist/assets/desktop-update-config-ChhfeOHj.js +1 -0
  11. package/dist/assets/{dist-Cy_e4kNb.js → dist-_UMsWSY0.js} +1 -1
  12. package/dist/assets/{dist-m08DUswB.js → dist-sD6V4nHX.js} +1 -1
  13. package/dist/assets/doc-browser-Bq7LJsQY.js +1 -0
  14. package/dist/assets/doc-browser-BuDgpY2e.js +1 -0
  15. package/dist/assets/{doc-browser-context-jiy-O4F8.js → doc-browser-context-Capim3og.js} +1 -1
  16. package/dist/assets/{extensions-CIb17__o.js → extensions-Bdclr3Wr.js} +1 -1
  17. package/dist/assets/{form-actions-DGf5zUg_.js → form-actions-CH0kWhEt.js} +1 -1
  18. package/dist/assets/index-BjxnLYE3.js +109 -0
  19. package/dist/assets/index-CBmwJjwE.css +1 -0
  20. package/dist/assets/loader-circle-CH_0iPLy.js +1 -0
  21. package/dist/assets/mcp-marketplace-page-BUsxLVB5.js +10 -0
  22. package/dist/assets/mcp-marketplace-page-GPRV3fB0.js +1 -0
  23. package/dist/assets/model-config-page-CIevwqp4.js +1 -0
  24. package/dist/assets/{navigation-link-DAfwFCj4.js → navigation-link-CAUiM3zS.js} +1 -1
  25. package/dist/assets/plus-B3obD55V.js +1 -0
  26. package/dist/assets/privacy-settings-page-CNKfZsYH.js +1 -0
  27. package/dist/assets/providers-config-page-DAah2NIS.js +1 -0
  28. package/dist/assets/react-Co6JiC1n.js +8 -0
  29. package/dist/assets/{refresh-cw-D_Eibznw.js → refresh-cw-BG_LX3SS.js} +1 -1
  30. package/dist/assets/remote-DuscgfvR.js +1 -0
  31. package/dist/assets/{rotate-cw-QQjmoWuq.js → rotate-cw-Cz7I_m7O.js} +1 -1
  32. package/dist/assets/runtime-config-page-Cl1RpmSC.js +1 -0
  33. package/dist/assets/{save-W2xthJJ2.js → save-BiHnb8ps.js} +1 -1
  34. package/dist/assets/{search-Vih1R2BD.js → search-C47TyRg0.js} +1 -1
  35. package/dist/assets/{search-config-page-CtBcL2uc.js → search-config-page-BAIzd6gN.js} +1 -1
  36. package/dist/assets/{secrets-config-page-Q6XfIj0h.js → secrets-config-page-Ddi-n43R.js} +2 -2
  37. package/dist/assets/security-config-C8MHnE1r.js +1 -0
  38. package/dist/assets/{select-lNdozU4o.js → select-BzQKfW1e.js} +2 -2
  39. package/dist/assets/skeleton-BXFAE9SE.js +1 -0
  40. package/dist/assets/{tag-chip-gdaU9bE7.js → tag-chip-C3K814HZ.js} +1 -1
  41. package/dist/assets/x-C1r9IXet.js +1 -0
  42. package/dist/index.html +29 -21
  43. package/dist/themes/island/island-atmosphere.webp +0 -0
  44. package/index.html +8 -0
  45. package/package.json +8 -8
  46. package/public/themes/island/island-atmosphere.webp +0 -0
  47. package/src/app/components/layout/header.tsx +4 -1
  48. package/src/app/components/layout/page-layout.tsx +4 -1
  49. package/src/app/components/layout/sidebar.tsx +6 -1
  50. package/src/app/configs/app-navigation.config.ts +17 -2
  51. package/src/app/hooks/use-feature-controls.ts +11 -0
  52. package/src/app/index.tsx +10 -0
  53. package/src/app/styles/island-theme.css +542 -0
  54. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +1 -0
  55. package/src/features/chat/components/conversation/chat-conversation-header.tsx +5 -1
  56. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +4 -1
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +10 -0
  58. package/src/features/chat/components/layout/chat-page-shell.tsx +4 -1
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +2 -0
  60. package/src/features/chat/components/layout/chat-sidebar.tsx +2 -0
  61. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +12 -0
  62. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +3 -0
  63. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +14 -1
  64. package/src/features/chat/features/session/utils/chat-session-deletion.utils.ts +25 -0
  65. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  66. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +1 -0
  67. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +1 -0
  68. package/src/features/chat/managers/__tests__/chat-thread-deletion.manager.test.ts +125 -0
  69. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +0 -37
  70. package/src/features/chat/managers/chat-thread.manager.ts +17 -28
  71. package/src/features/desktop-capabilities/components/desktop-authorization-dialog.test.tsx +58 -0
  72. package/src/features/desktop-capabilities/components/desktop-authorization-dialog.tsx +161 -0
  73. package/src/features/desktop-capabilities/hooks/use-desktop-capabilities.test.ts +10 -0
  74. package/src/features/desktop-capabilities/hooks/use-desktop-capabilities.ts +55 -0
  75. package/src/features/desktop-capabilities/index.ts +1 -0
  76. package/src/features/desktop-capabilities/managers/desktop-capability.manager.ts +25 -0
  77. package/src/features/desktop-capabilities/pages/desktop-capabilities-page.test.tsx +44 -0
  78. package/src/features/desktop-capabilities/pages/desktop-capabilities-page.tsx +171 -0
  79. package/src/features/desktop-capabilities/stores/desktop-authorization.store.ts +21 -0
  80. package/src/features/marketplace/components/mcp/__tests__/mcp-connection-dialog.test.tsx +23 -0
  81. package/src/features/marketplace/components/mcp/mcp-connection-dialog.tsx +35 -0
  82. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +20 -2
  83. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +9 -0
  84. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +1 -0
  85. package/src/features/service-apps/components/__tests__/service-apps-panel.test.tsx +35 -1
  86. package/src/features/service-apps/components/service-action-row.tsx +130 -0
  87. package/src/features/service-apps/components/service-app-list-item.tsx +11 -66
  88. package/src/features/service-apps/components/service-apps-panel.tsx +12 -0
  89. package/src/features/service-apps/hooks/use-service-apps.ts +20 -1
  90. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +2 -2
  91. package/src/features/side-dock/components/side-dock.tsx +1 -0
  92. package/src/index.css +1 -0
  93. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +8 -0
  94. package/src/platforms/desktop/components/desktop-app-shell.tsx +56 -2
  95. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -0
  96. package/src/platforms/mobile/components/mobile-settings-shell.tsx +5 -1
  97. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +3 -0
  98. package/src/shared/components/doc-browser/doc-browser.tsx +2 -0
  99. package/src/shared/components/ui/__tests__/confirm-dialog.test.tsx +31 -0
  100. package/src/shared/components/ui/confirm-dialog.tsx +11 -1
  101. package/src/shared/components/ui/context-menu/context-menu.tsx +1 -0
  102. package/src/shared/components/ui/dialog.tsx +2 -0
  103. package/src/shared/components/ui/overlays/sheet.tsx +2 -0
  104. package/src/shared/components/ui/popover.tsx +1 -0
  105. package/src/shared/components/ui/select.tsx +1 -0
  106. package/src/shared/components/ui/tooltip.tsx +1 -0
  107. package/src/shared/lib/api/index.ts +1 -0
  108. package/src/shared/lib/api/utils/mcp-connection.utils.ts +10 -0
  109. package/src/shared/lib/i18n/locales/en-US/chat.json +2 -0
  110. package/src/shared/lib/i18n/locales/en-US/core.json +44 -0
  111. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +1 -0
  112. package/src/shared/lib/i18n/locales/en-US/marketplace.json +29 -1
  113. package/src/shared/lib/i18n/locales/zh-CN/chat.json +2 -0
  114. package/src/shared/lib/i18n/locales/zh-CN/core.json +44 -0
  115. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +1 -0
  116. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +29 -1
  117. package/src/shared/lib/theme/index.test.ts +9 -0
  118. package/src/shared/lib/theme/index.ts +2 -0
  119. package/dist/assets/desktop-update-config-BiZmy7FC.js +0 -1
  120. package/dist/assets/doc-browser-CPsPHh12.js +0 -1
  121. package/dist/assets/doc-browser-npOvWtgf.js +0 -1
  122. package/dist/assets/index-C86hxgv5.css +0 -1
  123. package/dist/assets/index-CYBXR7zN.js +0 -109
  124. package/dist/assets/loader-circle-BlLuRFVW.js +0 -1
  125. package/dist/assets/mcp-marketplace-page-B12JNh7Q.js +0 -1
  126. package/dist/assets/mcp-marketplace-page-kLWW6kzt.js +0 -9
  127. package/dist/assets/model-config-page-DSeAjVT_.js +0 -1
  128. package/dist/assets/plus-D4LtWtTk.js +0 -1
  129. package/dist/assets/privacy-settings-page-tN0Mt15x.js +0 -1
  130. package/dist/assets/providers-config-page-CgPrXR1u.js +0 -1
  131. package/dist/assets/react-8iBFECZE.js +0 -8
  132. package/dist/assets/remote-Derr_AQp.js +0 -1
  133. package/dist/assets/runtime-config-page-Crq96Jjq.js +0 -1
  134. package/dist/assets/security-config-CE9rkQvq.js +0 -1
  135. package/dist/assets/skeleton-B__aTcly.js +0 -1
  136. package/dist/assets/x-CWdWBu_U.js +0 -1
@@ -40,6 +40,11 @@ describe("ChatSidebarToolbar", () => {
40
40
  expect(
41
41
  screen.getByPlaceholderText("Search conversations...").className,
42
42
  ).toContain("border-0");
43
+ expect(
44
+ screen
45
+ .getByPlaceholderText("Search conversations...")
46
+ .getAttribute("data-theme-control"),
47
+ ).toBe("chat-search");
43
48
  });
44
49
 
45
50
  it("keeps mobile search icon transparent to pointer input", () => {
@@ -49,5 +54,10 @@ describe("ChatSidebarToolbar", () => {
49
54
  expect(
50
55
  screen.getByPlaceholderText("Search conversations...").className,
51
56
  ).toContain("border-0");
57
+ expect(
58
+ screen
59
+ .getByPlaceholderText("Search conversations...")
60
+ .getAttribute("data-theme-control"),
61
+ ).toBe("chat-search");
52
62
  });
53
63
  });
@@ -53,7 +53,10 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
53
53
  {view === "chat" ? (
54
54
  isMobile ? <ChatMobileShell /> : <ChatConversationPanel />
55
55
  ) : (
56
- <section className="flex-1 min-h-0 overflow-hidden bg-background">
56
+ <section
57
+ data-theme-surface="workspace"
58
+ className="flex-1 min-h-0 overflow-hidden bg-background"
59
+ >
57
60
  {view === "inbox" ? (
58
61
  <div className={`mx-auto flex h-full min-h-0 w-full flex-col py-4 sm:px-6 sm:py-5 ${MANAGEMENT_PAGE_CANVAS_WIDTH_CLASS}`}>
59
62
  <InboxPage />
@@ -194,6 +194,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
194
194
  <div className="relative">
195
195
  <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/60" />
196
196
  <Input
197
+ data-theme-control="chat-search"
197
198
  value={query}
198
199
  onChange={(event) => onQueryChange(event.target.value)}
199
200
  placeholder={t("chatSidebarSearchPlaceholder")}
@@ -223,6 +224,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
223
224
  <div className="relative min-w-0 flex-1">
224
225
  <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
225
226
  <Input
227
+ data-theme-control="chat-search"
226
228
  value={query}
227
229
  onChange={(event) => onQueryChange(event.target.value)}
228
230
  placeholder={t("chatSidebarSearchPlaceholder")}
@@ -247,6 +247,7 @@ export function ChatSidebar({
247
247
  onTogglePinned={() =>
248
248
  presenter.chatSessionListManager.toggleSessionPinned(item.session.key)
249
249
  }
250
+ onDeleteSession={presenter.chatThreadManager.deleteSession}
250
251
  />
251
252
  );
252
253
  const createSessionAndOpenIfNeeded = (
@@ -278,6 +279,7 @@ export function ChatSidebar({
278
279
 
279
280
  return (
280
281
  <aside
282
+ data-theme-surface="navigation"
281
283
  className={cn(
282
284
  "flex h-full min-h-0 flex-col bg-secondary transition-[width] duration-200 ease-out",
283
285
  isMobileVariant
@@ -5,6 +5,7 @@ import { ChatSidebarSessionItem } from "@/features/chat/features/session/compone
5
5
 
6
6
  const mocks = vi.hoisted(() => ({
7
7
  copyText: vi.fn(),
8
+ deleteSession: vi.fn(),
8
9
  }));
9
10
 
10
11
  vi.mock("@nextclaw/agent-chat-ui", () => ({
@@ -21,6 +22,7 @@ vi.mock("sonner", () => ({
21
22
  beforeEach(() => {
22
23
  mocks.copyText.mockReset();
23
24
  mocks.copyText.mockResolvedValue(true);
25
+ mocks.deleteSession.mockReset();
24
26
  render(
25
27
  <ChatSidebarSessionItem
26
28
  sessionKey="session:current"
@@ -50,6 +52,7 @@ beforeEach(() => {
50
52
  onSave={vi.fn()}
51
53
  onCancel={vi.fn()}
52
54
  onTogglePinned={vi.fn()}
55
+ onDelete={mocks.deleteSession}
53
56
  />,
54
57
  );
55
58
  });
@@ -87,6 +90,15 @@ it("copies the sidebar session ID from the more-actions menu", async () => {
87
90
  expect(mocks.copyText).toHaveBeenCalledWith("session:current");
88
91
  });
89
92
 
93
+ it("deletes the sidebar session from the more-actions menu", async () => {
94
+ const user = userEvent.setup();
95
+
96
+ await user.click(screen.getByRole("button", { name: "More actions" }));
97
+ await user.click(screen.getByRole("button", { name: "Delete Session" }));
98
+
99
+ expect(mocks.deleteSession).toHaveBeenCalledOnce();
100
+ });
101
+
90
102
  it("sizes the runtime icon to the session title text", () => {
91
103
  const runtimeIcon = screen.getByRole("img", { name: "Codex logo" });
92
104
 
@@ -32,6 +32,7 @@ export function ChatSidebarSessionEntry(props: {
32
32
  onSaveSessionLabel: (session: SessionEntryView) => void;
33
33
  onCancelEditingSessionLabel: () => void;
34
34
  onTogglePinned: () => void;
35
+ onDeleteSession: (sessionKey: string) => void;
35
36
  }) {
36
37
  const {
37
38
  item,
@@ -52,6 +53,7 @@ export function ChatSidebarSessionEntry(props: {
52
53
  onSaveSessionLabel,
53
54
  onCancelEditingSessionLabel,
54
55
  onTogglePinned,
56
+ onDeleteSession,
55
57
  } = props;
56
58
  const { session, runStatus } = item;
57
59
  const active = selectedSessionKey === session.key;
@@ -111,6 +113,7 @@ export function ChatSidebarSessionEntry(props: {
111
113
  onSave={() => onSaveSessionLabel(session)}
112
114
  onCancel={onCancelEditingSessionLabel}
113
115
  onTogglePinned={onTogglePinned}
116
+ onDelete={() => onDeleteSession(session.key)}
114
117
  />
115
118
  );
116
119
  }
@@ -15,9 +15,11 @@ import {
15
15
  GitBranch,
16
16
  Pencil,
17
17
  Pin,
18
+ Trash2,
18
19
  X,
19
20
  } from "lucide-react";
20
21
  import { ChatSidebarContextCard } from "@/features/chat/features/session/components/chat-sidebar-context-card";
22
+ import { ChatSessionHeaderMenuItem } from "@/features/chat/features/session/components/session-header/chat-session-header-menu-item";
21
23
 
22
24
  type ChatSidebarSessionItemProps = {
23
25
  sessionKey: string;
@@ -44,6 +46,7 @@ type ChatSidebarSessionItemProps = {
44
46
  onSave: () => void | Promise<void>;
45
47
  onCancel: () => void;
46
48
  onTogglePinned: () => void;
49
+ onDelete: () => void;
47
50
  };
48
51
 
49
52
  type ChatSidebarSessionEditingViewProps = Pick<
@@ -139,6 +142,7 @@ function ChatSidebarSessionDisplayView({
139
142
  onSelect,
140
143
  onStartEditing,
141
144
  onTogglePinned,
145
+ onDelete,
142
146
  }: ChatSidebarSessionDisplayViewProps) {
143
147
  return (
144
148
  <div className="group/session relative">
@@ -262,7 +266,14 @@ function ChatSidebarSessionDisplayView({
262
266
  sessionKey={sessionKey}
263
267
  triggerSize="sm"
264
268
  triggerTone="strong"
265
- />
269
+ >
270
+ <ChatSessionHeaderMenuItem
271
+ icon={Trash2}
272
+ label={t("chatDeleteSession")}
273
+ onClick={onDelete}
274
+ destructive
275
+ />
276
+ </ChatSessionMoreActionsMenu>
266
277
  </div>
267
278
  </div>
268
279
  );
@@ -293,6 +304,7 @@ export function ChatSidebarSessionItem({
293
304
  onSave,
294
305
  onCancel,
295
306
  onTogglePinned,
307
+ onDelete,
296
308
  }: ChatSidebarSessionItemProps) {
297
309
  return (
298
310
  <div
@@ -332,6 +344,7 @@ export function ChatSidebarSessionItem({
332
344
  projectName={projectName}
333
345
  onStartEditing={onStartEditing}
334
346
  onTogglePinned={onTogglePinned}
347
+ onDelete={onDelete}
335
348
  />
336
349
  )}
337
350
  </div>
@@ -0,0 +1,25 @@
1
+ import type { ChatThreadSnapshot } from "@/features/chat/stores/chat-thread.store";
2
+
3
+ export function createDeletedChatThreadStatePatch(): Partial<ChatThreadSnapshot> {
4
+ return {
5
+ sessionKey: null,
6
+ sessionTypeLabel: null,
7
+ agentId: null,
8
+ sessionDisplayName: undefined,
9
+ draftProjectRoot: null,
10
+ sessionProjectName: null,
11
+ canDeleteSession: false,
12
+ parentSessionKey: null,
13
+ parentSessionLabel: null,
14
+ workspacePanelParentKey: null,
15
+ activeWorkspacePanelKind: null,
16
+ childSessionTabs: [],
17
+ activeChildSessionKey: null,
18
+ activeSideChatDraft: null,
19
+ workspaceFileTabs: [],
20
+ activeWorkspaceFileKey: null,
21
+ closedWorkspaceTabEntries: [],
22
+ workspaceNavigationHistory: [],
23
+ workspaceNavigationHistoryIndex: 0,
24
+ };
25
+ }
@@ -163,6 +163,7 @@ describe("ChatSessionWorkspacePanel", () => {
163
163
 
164
164
  const panel = screen.getByTestId("chat-session-workspace-panel");
165
165
  expect(panel.className).not.toContain("absolute");
166
+ expect(panel.getAttribute("data-theme-surface")).toBe("workspace-panel");
166
167
 
167
168
  await user.click(
168
169
  screen.getByRole("button", { name: "Maximize workspace panel" }),
@@ -363,6 +363,7 @@ export function ChatSessionWorkspacePanelContent({
363
363
  ) : null}
364
364
  <div
365
365
  data-testid="workspace-shared-explorer"
366
+ data-theme-surface="workspace-explorer"
366
367
  data-mode={projectFilesActive ? 'full' : explorerOverlay ? 'overlay' : 'side'}
367
368
  className={cn(
368
369
  'relative min-h-0 shrink-0 flex-col border-r border-gray-200/80 bg-white',
@@ -187,6 +187,7 @@ export function ChatSessionWorkspacePanel({
187
187
  return (
188
188
  <ResizableRightPanel
189
189
  data-testid="chat-session-workspace-panel"
190
+ data-theme-surface="workspace-panel"
190
191
  className={cn(
191
192
  isOverlayPanel
192
193
  ? "bg-white"
@@ -0,0 +1,125 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { appQueryClient } from "@/app-query-client";
3
+ import { ChatThreadManager } from "@/features/chat/managers/chat-thread.manager";
4
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
6
+ import type * as SharedApi from "@/shared/lib/api";
7
+
8
+ const { deleteNcpSessionMock, deleteSummaryMock, toast } = vi.hoisted(() => ({
9
+ deleteNcpSessionMock: vi.fn(),
10
+ deleteSummaryMock: vi.fn(),
11
+ toast: { success: vi.fn(), error: vi.fn() },
12
+ }));
13
+
14
+ vi.mock("@/shared/lib/api", async (importOriginal) => {
15
+ const actual = await importOriginal<typeof SharedApi>();
16
+ return {
17
+ ...actual,
18
+ deleteNcpSession: deleteNcpSessionMock,
19
+ deleteNcpSessionSummaryInQueryClient: deleteSummaryMock,
20
+ };
21
+ });
22
+
23
+ vi.mock("sonner", () => ({ toast }));
24
+
25
+ function createUiManager() {
26
+ return {
27
+ confirm: vi.fn(async () => true),
28
+ goToChatRoot: vi.fn(),
29
+ } as unknown as ConstructorParameters<typeof ChatThreadManager>[0];
30
+ }
31
+
32
+ beforeEach(() => {
33
+ deleteNcpSessionMock.mockReset();
34
+ deleteNcpSessionMock.mockResolvedValue({
35
+ deleted: true,
36
+ sessionId: "parent-session-1",
37
+ });
38
+ deleteSummaryMock.mockReset();
39
+ toast.success.mockReset();
40
+ toast.error.mockReset();
41
+ useChatSessionListStore.setState({
42
+ snapshot: {
43
+ ...useChatSessionListStore.getInitialState().snapshot,
44
+ selectedSessionKey: "parent-session-1",
45
+ },
46
+ });
47
+ useChatThreadStore.setState({
48
+ snapshot: {
49
+ ...useChatThreadStore.getInitialState().snapshot,
50
+ sessionKey: "parent-session-1",
51
+ canDeleteSession: true,
52
+ childSessionTabs: [
53
+ {
54
+ sessionKey: "child-session-1",
55
+ parentSessionKey: "parent-session-1",
56
+ label: "Child Session 1",
57
+ agentId: "reviewer",
58
+ },
59
+ ],
60
+ },
61
+ });
62
+ });
63
+
64
+ describe("ChatThreadManager deletion", () => {
65
+ it("clears the current thread and confirms the deletion", async () => {
66
+ const uiManager = createUiManager();
67
+ const removeQueries = vi
68
+ .spyOn(appQueryClient, "removeQueries")
69
+ .mockImplementation(async () => undefined);
70
+ const manager = new ChatThreadManager(
71
+ uiManager,
72
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
73
+ );
74
+
75
+ await manager.deleteSession();
76
+
77
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
78
+ expect(deleteSummaryMock).toHaveBeenCalledWith(
79
+ appQueryClient,
80
+ "parent-session-1",
81
+ );
82
+ expect(removeQueries).toHaveBeenCalledWith({
83
+ queryKey: ["ncp-session-messages", "parent-session-1"],
84
+ });
85
+ expect(uiManager.goToChatRoot).toHaveBeenCalledWith({ replace: true });
86
+ expect(toast.success).toHaveBeenCalledWith("Session deleted.");
87
+ });
88
+
89
+ it("preserves the current thread when deleting another session", async () => {
90
+ const uiManager = createUiManager();
91
+ const manager = new ChatThreadManager(
92
+ uiManager,
93
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
94
+ );
95
+
96
+ await manager.deleteSession("other-session-1");
97
+
98
+ expect(deleteNcpSessionMock).toHaveBeenCalledWith("other-session-1");
99
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBe(
100
+ "parent-session-1",
101
+ );
102
+ expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it("keeps the session visible and reports a deletion error", async () => {
106
+ deleteNcpSessionMock.mockRejectedValueOnce(
107
+ new Error("network unavailable"),
108
+ );
109
+ const uiManager = createUiManager();
110
+ const manager = new ChatThreadManager(
111
+ uiManager,
112
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
113
+ );
114
+
115
+ await manager.deleteSession();
116
+
117
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBe(
118
+ "parent-session-1",
119
+ );
120
+ expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
121
+ expect(toast.error).toHaveBeenCalledWith(
122
+ "Could not delete session: network unavailable",
123
+ );
124
+ });
125
+ });
@@ -1,5 +1,4 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { appQueryClient } from '@/app-query-client';
3
2
  import { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
4
3
  import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
5
4
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
@@ -841,39 +840,3 @@ describe('ChatThreadManager visible workspace selection', () => {
841
840
  expect(sessionListManager.markVisibleWorkspaceChildRead).toHaveBeenCalledWith(tab);
842
841
  });
843
842
  });
844
-
845
- describe('ChatThreadManager deletion', () => {
846
- it('clears thread state while leaving route-derived selection to navigation', async () => {
847
- const removeQueries = vi.spyOn(appQueryClient, 'removeQueries').mockImplementation(async () => undefined);
848
- const uiManager = {
849
- goToSession: vi.fn(),
850
- goToChatRoot: vi.fn(),
851
- goToProviders: vi.fn(),
852
- confirm: vi.fn(async () => true),
853
- } as unknown as ConstructorParameters<typeof ChatThreadManager>[0];
854
- const sessionListManager = {} as ConstructorParameters<typeof ChatThreadManager>[1];
855
- const manager = new ChatThreadManager(uiManager, sessionListManager);
856
-
857
- await manager.deleteSession();
858
-
859
- expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('parent-session-1');
860
- expect(useChatThreadStore.getState().snapshot).toMatchObject({
861
- sessionKey: null,
862
- canDeleteSession: false,
863
- workspacePanelParentKey: null,
864
- childSessionTabs: [],
865
- activeChildSessionKey: null,
866
- workspaceFileTabs: [],
867
- activeWorkspaceFileKey: null,
868
- workspaceNavigationHistory: [],
869
- workspaceNavigationHistoryIndex: 0,
870
- });
871
- expect(deleteSummaryMock).toHaveBeenCalledWith(appQueryClient, 'parent-session-1');
872
- expect(removeQueries).toHaveBeenCalledWith({
873
- queryKey: ['ncp-session-messages', 'parent-session-1'],
874
- });
875
- expect(uiManager.goToChatRoot).toHaveBeenCalledWith({ replace: true });
876
-
877
- removeQueries.mockRestore();
878
- });
879
- });
@@ -1,5 +1,6 @@
1
1
  import { appQueryClient } from '@/app-query-client';
2
2
  import { deleteNcpSession as deleteNcpSessionApi, deleteNcpSessionSummaryInQueryClient } from '@/shared/lib/api';
3
+ import { toast } from 'sonner';
3
4
  import type {
4
5
  ChatFileOpenActionViewModel,
5
6
  ChatUiShowContentRequest,
@@ -17,6 +18,7 @@ import type {
17
18
  ChatWorkspaceFileTab,
18
19
  } from '@/features/chat/stores/chat-thread.store';
19
20
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
21
+ import { createDeletedChatThreadStatePatch } from '@/features/chat/features/session/utils/chat-session-deletion.utils';
20
22
  import {
21
23
  normalizeChatWorkspaceExplorerWidth,
22
24
  normalizeChatWorkspacePanelWidth,
@@ -80,27 +82,7 @@ export class ChatThreadManager {
80
82
  };
81
83
 
82
84
  private clearDeletedSessionState = () => {
83
- useChatThreadStore.getState().setSnapshot({
84
- sessionKey: null,
85
- sessionTypeLabel: null,
86
- agentId: null,
87
- sessionDisplayName: undefined,
88
- draftProjectRoot: null,
89
- sessionProjectName: null,
90
- canDeleteSession: false,
91
- parentSessionKey: null,
92
- parentSessionLabel: null,
93
- workspacePanelParentKey: null,
94
- activeWorkspacePanelKind: null,
95
- childSessionTabs: [],
96
- activeChildSessionKey: null,
97
- activeSideChatDraft: null,
98
- workspaceFileTabs: [],
99
- activeWorkspaceFileKey: null,
100
- closedWorkspaceTabEntries: [],
101
- workspaceNavigationHistory: [],
102
- workspaceNavigationHistoryIndex: 0,
103
- });
85
+ useChatThreadStore.getState().setSnapshot(createDeletedChatThreadStatePatch());
104
86
  };
105
87
 
106
88
  private resolveWorkspaceParentSessionKey = (): string | null => {
@@ -565,11 +547,12 @@ export class ChatThreadManager {
565
547
  this.uiManager.goToSession(resolvedParentSessionKey);
566
548
  };
567
549
 
568
- deleteSession = async () => {
550
+ deleteSession = async (rawSessionKey?: string) => {
569
551
  const {
570
552
  snapshot: { selectedSessionKey },
571
553
  } = useChatSessionListStore.getState();
572
- if (!selectedSessionKey) {
554
+ const sessionKey = rawSessionKey?.trim() || selectedSessionKey;
555
+ if (!sessionKey) {
573
556
  return;
574
557
  }
575
558
  const confirmed = await this.uiManager.confirm({
@@ -582,13 +565,19 @@ export class ChatThreadManager {
582
565
  }
583
566
  useChatThreadStore.getState().setSnapshot({ isDeletePending: true });
584
567
  try {
585
- await deleteNcpSessionApi(selectedSessionKey);
586
- deleteNcpSessionSummaryInQueryClient(appQueryClient, selectedSessionKey);
568
+ await deleteNcpSessionApi(sessionKey);
569
+ deleteNcpSessionSummaryInQueryClient(appQueryClient, sessionKey);
587
570
  appQueryClient.removeQueries({
588
- queryKey: ['ncp-session-messages', selectedSessionKey],
571
+ queryKey: ['ncp-session-messages', sessionKey],
589
572
  });
590
- this.clearDeletedSessionState();
591
- this.uiManager.goToChatRoot({ replace: true });
573
+ toast.success(t('chatDeleteSessionSucceeded'));
574
+ if (sessionKey === selectedSessionKey) {
575
+ this.clearDeletedSessionState();
576
+ this.uiManager.goToChatRoot({ replace: true });
577
+ }
578
+ } catch (error) {
579
+ const message = error instanceof Error ? error.message : String(error);
580
+ toast.error(`${t('chatDeleteSessionFailed')}: ${message}`);
592
581
  } finally {
593
582
  useChatThreadStore.getState().setSnapshot({ isDeletePending: false });
594
583
  }
@@ -0,0 +1,58 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { DesktopAuthorizationDialog } from "./desktop-authorization-dialog";
4
+ import { useDesktopAuthorizationStore } from "../stores/desktop-authorization.store";
5
+
6
+ const mocks = vi.hoisted(() => ({ grantAccess: vi.fn() }));
7
+
8
+ vi.mock("../managers/desktop-capability.manager", () => ({
9
+ desktopCapabilityManager: {
10
+ grantAccess: mocks.grantAccess,
11
+ },
12
+ }));
13
+
14
+ describe("DesktopAuthorizationDialog", () => {
15
+ beforeEach(() => {
16
+ mocks.grantAccess.mockReset();
17
+ mocks.grantAccess.mockResolvedValue({});
18
+ useDesktopAuthorizationStore.getState().clear();
19
+ });
20
+
21
+ it("shows the trusted subject and grants only after explicit approval", async () => {
22
+ useDesktopAuthorizationStore.getState().present({
23
+ applicationId: "wechat",
24
+ request: {
25
+ subject: { type: "agent", id: "main" },
26
+ resource: {
27
+ type: "desktop.application",
28
+ target: { applicationId: "wechat", platform: "darwin", bundleId: "com.tencent.xinWeChat" },
29
+ },
30
+ access: ["ui.write"],
31
+ declarationFingerprint: "fingerprint",
32
+ },
33
+ });
34
+
35
+ render(<DesktopAuthorizationDialog />);
36
+ expect(screen.getByText("AI · main")).toBeTruthy();
37
+ expect(screen.getByText("wechat")).toBeTruthy();
38
+ expect(mocks.grantAccess).not.toHaveBeenCalled();
39
+
40
+ fireEvent.click(screen.getByRole("button", { name: "Allow" }));
41
+ await waitFor(() => expect(mocks.grantAccess).toHaveBeenCalledOnce());
42
+ });
43
+
44
+ it("explains the bounded pointer permission in user terms", () => {
45
+ useDesktopAuthorizationStore.getState().present({
46
+ applicationId: "wechat",
47
+ request: {
48
+ subject: { type: "agent", id: "main" },
49
+ resource: { type: "desktop.application", target: { applicationId: "wechat", platform: "darwin", bundleId: "com.tencent.xinWeChat" } },
50
+ access: ["input.pointer"],
51
+ declarationFingerprint: "fingerprint",
52
+ },
53
+ });
54
+ render(<DesktopAuthorizationDialog />);
55
+ expect(screen.getByText("Click inside the app window")).toBeTruthy();
56
+ expect(screen.getByText(/cannot use keys, scroll, drag, send, or confirm/i)).toBeTruthy();
57
+ });
58
+ });