@nextclaw/ui 0.15.0 → 0.15.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 (99) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/assets/{api-DLtsiBKm.js → api-b1R9VdeH.js} +1 -1
  3. package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-D2O9pch9.js} +1 -1
  4. package/dist/assets/{book-open-BQwWBDKb.js → book-open-B77lQ2Tg.js} +1 -1
  5. package/dist/assets/{channels-list-page-Cv8o9Hxg.js → channels-list-page-DcsIQc6A.js} +2 -2
  6. package/dist/assets/chat-page-CZ7WxPir.js +1 -0
  7. package/dist/assets/{config-split-page-EidRBpP2.js → config-split-page-BWQ6_Raq.js} +1 -1
  8. package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-C-NXSioX.js} +1 -1
  9. package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
  10. package/dist/assets/{dist-CBxWOazz.js → dist-YtoqU-AT.js} +1 -1
  11. package/dist/assets/{dist-D2eZC3OR.js → dist-jdGmbb6w.js} +1 -1
  12. package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
  13. package/dist/assets/{doc-browser-B76XOXmW.js → doc-browser-BdZPFOVa.js} +1 -1
  14. package/dist/assets/{doc-browser-context-Sv-044Cl.js → doc-browser-context-DwokemUn.js} +1 -1
  15. package/dist/assets/{es2015-DhQo_oS6.js → es2015-bNpL2LKy.js} +1 -1
  16. package/dist/assets/{external-link-CM5dnUI6.js → external-link-BM6fk8q-.js} +1 -1
  17. package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-CntWolRA.js} +1 -1
  18. package/dist/assets/index-BIcEw90u.js +104 -0
  19. package/dist/assets/index-BiNSTKTU.css +1 -0
  20. package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
  21. package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-CLmUstLN.js} +1 -1
  22. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
  23. package/dist/assets/{model-config-page-Cdr9P6TQ.js → model-config-page-Di0ySJD2.js} +1 -1
  24. package/dist/assets/plus-vDqW5Kxp.js +1 -0
  25. package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-Cm_a4GHf.js} +1 -1
  26. package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
  27. package/dist/assets/{react-DjpzoZXB.js → react-BN8uzPnp.js} +1 -1
  28. package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-BpVcFPmJ.js} +1 -1
  29. package/dist/assets/remote-CLQ0IBM8.js +1 -0
  30. package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
  31. package/dist/assets/{runtime-config-page-CfTqh1nv.js → runtime-config-page-CflvvoDc.js} +1 -1
  32. package/dist/assets/{save-BZof1uie.js → save-BpFW5fpk.js} +1 -1
  33. package/dist/assets/{search-MxAarS_g.js → search-D0uO7enf.js} +1 -1
  34. package/dist/assets/{search-config-page-Dm8daYny.js → search-config-page-D0klFUXd.js} +1 -1
  35. package/dist/assets/{secrets-config-page-D8AbcR05.js → secrets-config-page-dVZ631Wp.js} +1 -1
  36. package/dist/assets/security-config-C5gnlkuj.js +1 -0
  37. package/dist/assets/{select-D7GcNP0z.js → select-Bh-CrA4p.js} +1 -1
  38. package/dist/assets/{setting-row-CuozlnJd.js → setting-row-BJfCHRcb.js} +1 -1
  39. package/dist/assets/settings-2-Blzi1vr6.js +1 -0
  40. package/dist/assets/{side-dock-BcLau5S_.js → side-dock-ZdaSrQVB.js} +1 -1
  41. package/dist/assets/{skeleton-shfmXuYp.js → skeleton-BLlEzebZ.js} +1 -1
  42. package/dist/assets/{switch-C1BWMU5c.js → switch-CFLJFNSd.js} +1 -1
  43. package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-CzGihiQx.js} +1 -1
  44. package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CdcrBjrC.js} +1 -1
  45. package/dist/assets/{use-config-DAdbT5QJ.js → use-config-CVPgsBmU.js} +1 -1
  46. package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CSeBk9SF.js} +1 -1
  47. package/dist/assets/x-BF2KQ--V.js +1 -0
  48. package/dist/index.html +26 -26
  49. package/package.json +9 -9
  50. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  51. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  52. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  53. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  54. package/src/features/agents/components/agents-page.tsx +76 -10
  55. package/src/features/agents/types/agent-form.types.ts +21 -0
  56. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  57. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
  58. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
  59. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
  61. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
  62. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
  63. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  64. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  65. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  66. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  67. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  68. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  69. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  70. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +34 -0
  71. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +1 -7
  72. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  73. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
  74. package/src/features/chat/managers/chat-thread.manager.ts +8 -2
  75. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  76. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  77. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  78. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  79. package/src/shared/lib/api/types.ts +12 -3
  80. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  81. package/src/shared/lib/i18n/locales/en-US/chat.json +3 -0
  82. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  83. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  84. package/src/shared/lib/i18n/locales/zh-CN/chat.json +3 -0
  85. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  86. package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
  87. package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
  88. package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
  89. package/dist/assets/ellipsis-2uzhypw5.js +0 -1
  90. package/dist/assets/index-CBRP-7-a.css +0 -1
  91. package/dist/assets/index-Z1fYpbNd.js +0 -104
  92. package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
  93. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
  94. package/dist/assets/plus-3ztRZD_l.js +0 -1
  95. package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
  96. package/dist/assets/remote-CKWZfVVB.js +0 -1
  97. package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
  98. package/dist/assets/security-config-Ds0LsJmo.js +0 -1
  99. package/dist/assets/x-DqL1mYLC.js +0 -1
@@ -7,12 +7,17 @@ import { ChatConversationHeaderSection } from "@/features/chat/components/conver
7
7
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
8
8
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
9
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
10
- import type { NcpSessionSummaryView } from "@/shared/lib/api";
10
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
11
+ import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
12
+ import type { NcpSessionSummaryView, SessionEntryView } from "@/shared/lib/api";
11
13
 
12
14
  const mocks = vi.hoisted(() => ({
13
15
  deleteSession: vi.fn(),
14
16
  openChildSessionPanel: vi.fn(),
15
17
  openSessionCronPanel: vi.fn(),
18
+ selectSession: vi.fn(),
19
+ sessionItems: [] as NcpSessionListItemView[],
20
+ isSessionListLoading: false,
16
21
  }));
17
22
 
18
23
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
@@ -22,9 +27,35 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
22
27
  openChildSessionPanel: mocks.openChildSessionPanel,
23
28
  openSessionCronPanel: mocks.openSessionCronPanel,
24
29
  },
30
+ chatSessionListManager: {
31
+ selectSession: mocks.selectSession,
32
+ },
25
33
  }),
26
34
  }));
27
35
 
36
+ vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-list-view", () => ({
37
+ useNcpSessionListView: (params?: { query?: string | null }) => {
38
+ const query = params?.query?.trim().toLowerCase() ?? "";
39
+ const items = query
40
+ ? mocks.sessionItems.filter(({ session }) =>
41
+ [
42
+ session.key,
43
+ session.label ?? "",
44
+ session.projectRoot ?? "",
45
+ session.projectName ?? "",
46
+ ]
47
+ .join(" ")
48
+ .toLowerCase()
49
+ .includes(query),
50
+ )
51
+ : mocks.sessionItems;
52
+ return {
53
+ isLoading: mocks.isSessionListLoading,
54
+ items,
55
+ };
56
+ },
57
+ }));
58
+
28
59
  vi.mock("@/features/cron", () => ({
29
60
  useCronJobs: () => ({ data: { jobs: [] } }),
30
61
  }));
@@ -54,6 +85,24 @@ function createSummary(
54
85
  };
55
86
  }
56
87
 
88
+ function createSessionListItem(
89
+ overrides: Partial<SessionEntryView> & Pick<SessionEntryView, "key">,
90
+ ): NcpSessionListItemView {
91
+ return {
92
+ session: {
93
+ createdAt: "2026-06-18T00:00:00.000Z",
94
+ updatedAt: "2026-06-18T00:00:00.000Z",
95
+ lastMessageAt: "2026-06-18T00:00:00.000Z",
96
+ readAt: "2026-06-18T00:00:00.000Z",
97
+ sessionType: "native",
98
+ sessionTypeMutable: false,
99
+ messageCount: 1,
100
+ ...overrides,
101
+ },
102
+ runStatus: overrides.status === "running" ? "running" : undefined,
103
+ };
104
+ }
105
+
57
106
  function renderHeaderSection() {
58
107
  const queryClient = new QueryClient({
59
108
  defaultOptions: {
@@ -73,6 +122,9 @@ describe("ChatConversationHeaderSection", () => {
73
122
  mocks.deleteSession.mockReset();
74
123
  mocks.openChildSessionPanel.mockReset();
75
124
  mocks.openSessionCronPanel.mockReset();
125
+ mocks.selectSession.mockReset();
126
+ mocks.isSessionListLoading = false;
127
+ viewportLayoutManager.resetForTests();
76
128
  useChatSessionListStore.persist.setOptions({
77
129
  storage: new MemoryPersistStorage(),
78
130
  });
@@ -129,6 +181,18 @@ describe("ChatConversationHeaderSection", () => {
129
181
  } as never,
130
182
  },
131
183
  });
184
+ mocks.sessionItems = [
185
+ createSessionListItem({
186
+ key: "parent-session-1",
187
+ label: "Parent Task",
188
+ }),
189
+ createSessionListItem({
190
+ key: "session:ncp-2",
191
+ label: "Background Task",
192
+ lastMessageAt: "2026-06-18T01:00:00.000Z",
193
+ readAt: "2026-06-18T01:00:00.000Z",
194
+ }),
195
+ ];
132
196
  });
133
197
 
134
198
  it("shows the child-session entry from session query state before a tool card opens it", async () => {
@@ -142,4 +206,71 @@ describe("ChatConversationHeaderSection", () => {
142
206
  activeChildSessionKey: "child-session-1",
143
207
  });
144
208
  });
209
+
210
+ it("uses the collapsed desktop title as a session switcher", async () => {
211
+ const user = userEvent.setup();
212
+ viewportLayoutManager.setSidebarCollapsed(true);
213
+
214
+ renderHeaderSection();
215
+
216
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
217
+ await user.click(screen.getByRole("button", { name: /Background Task/ }));
218
+
219
+ expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
220
+ });
221
+
222
+ it("keeps the desktop title plain while the sidebar is expanded", () => {
223
+ renderHeaderSection();
224
+
225
+ expect(screen.queryByRole("button", { name: /Switch session/ })).toBeNull();
226
+ expect(screen.getByText("Parent Task")).toBeTruthy();
227
+ });
228
+
229
+ it("keeps session switching available from a collapsed new-session header", async () => {
230
+ const user = userEvent.setup();
231
+ viewportLayoutManager.setSidebarCollapsed(true);
232
+ useChatSessionListStore.setState({
233
+ snapshot: {
234
+ ...useChatSessionListStore.getState().snapshot,
235
+ selectedSessionKey: null,
236
+ },
237
+ });
238
+ useChatThreadStore.setState({
239
+ snapshot: {
240
+ ...useChatThreadStore.getState().snapshot,
241
+ sessionKey: null,
242
+ },
243
+ });
244
+
245
+ renderHeaderSection();
246
+
247
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
248
+ await user.click(screen.getByRole("button", { name: /Background Task/ }));
249
+
250
+ expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
251
+ });
252
+
253
+ it("filters the collapsed title switcher with a local search query", async () => {
254
+ const user = userEvent.setup();
255
+ viewportLayoutManager.setSidebarCollapsed(true);
256
+
257
+ renderHeaderSection();
258
+
259
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
260
+ const searchInput = screen.getByRole("textbox", {
261
+ name: "Search session key / label",
262
+ });
263
+ await user.type(searchInput, "Background");
264
+ await user.click(screen.getByRole("button", { name: /Background Task/ }));
265
+
266
+ expect(mocks.selectSession).toHaveBeenCalledWith("session:ncp-2");
267
+
268
+ await user.click(screen.getByRole("button", { name: /Switch session/ }));
269
+ await user.type(
270
+ screen.getByRole("textbox", { name: "Search session key / label" }),
271
+ "missing",
272
+ );
273
+
274
+ expect(screen.getByText("No matching sessions")).toBeTruthy();
275
+ });
145
276
  });
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
  import { ChatConversationHeader } from "@/features/chat/components/conversation/chat-conversation-header";
3
3
  import { ChatSessionHeaderActions } from "@/features/chat/features/session/components/session-header/chat-session-header-actions";
4
4
  import { ChatSessionProjectBadge } from "@/features/chat/features/session/components/session-header/chat-session-project-badge";
5
+ import { ChatSessionTitleSwitcher } from "@/features/chat/features/session/components/session-header/chat-session-title-switcher";
5
6
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
6
7
  import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
7
8
  import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
@@ -89,6 +90,13 @@ export function ChatConversationHeaderSection({
89
90
  <ChatConversationHeader
90
91
  layoutMode={layoutMode}
91
92
  title={sessionHeaderTitle}
93
+ titleContent={
94
+ <ChatSessionTitleSwitcher
95
+ layoutMode={layoutMode}
96
+ selectedSessionKey={sessionKey}
97
+ title={sessionHeaderTitle}
98
+ />
99
+ }
92
100
  shouldShow={shouldShowSessionHeader}
93
101
  onBackToList={onBackToList}
94
102
  leading={
@@ -41,6 +41,7 @@ export function ChatConversationHeader({
41
41
  sessionTypeBadge,
42
42
  shouldShow,
43
43
  title,
44
+ titleContent,
44
45
  }: {
45
46
  layoutMode: "desktop" | "mobile";
46
47
  actions?: ReactNode;
@@ -50,6 +51,7 @@ export function ChatConversationHeader({
50
51
  sessionTypeBadge?: ReactNode;
51
52
  shouldShow: boolean;
52
53
  title: string;
54
+ titleContent?: ReactNode;
53
55
  }) {
54
56
  const isMobileLayout = layoutMode === "mobile";
55
57
 
@@ -78,9 +80,11 @@ export function ChatConversationHeader({
78
80
  />
79
81
  ) : null}
80
82
  {leading}
81
- <span className="text-sm font-medium text-foreground truncate">
82
- {title}
83
- </span>
83
+ {titleContent ?? (
84
+ <span className="text-sm font-medium text-foreground truncate">
85
+ {title}
86
+ </span>
87
+ )}
84
88
  {sessionTypeBadge}
85
89
  {projectBadge}
86
90
  </div>
@@ -50,7 +50,7 @@ const mocks = vi.hoisted(() => {
50
50
  modelOptions: [],
51
51
  selectedSession: null as null | {
52
52
  activityPreview?: {
53
- state: 'running' | 'completed' | 'failed' | 'idle';
53
+ state: 'running' | 'completed' | 'failed' | 'cancelled' | 'idle';
54
54
  statusText?: string;
55
55
  replyText?: string;
56
56
  };
@@ -260,7 +260,7 @@ describe('SessionConversationArea input boundary', () => {
260
260
  mocks.inputQuery.selectedSession = {
261
261
  activityPreview: {
262
262
  state: 'failed',
263
- statusText: '运行出错:Invalid API Key',
263
+ statusText: 'Run failed: Invalid API Key',
264
264
  },
265
265
  status: 'idle',
266
266
  };
@@ -269,6 +269,21 @@ describe('SessionConversationArea input boundary', () => {
269
269
 
270
270
  expect(screen.getByTestId('conversation-bottom-slot')).toBeTruthy();
271
271
  expect(screen.getByText(/出错了|Something went wrong/)).toBeTruthy();
272
- expect(screen.getByText('运行出错:Invalid API Key')).toBeTruthy();
272
+ expect(screen.getByText('Run failed: Invalid API Key')).toBeTruthy();
273
+ });
274
+
275
+ it('does not surface user-cancelled previews as conversation errors', () => {
276
+ mocks.inputQuery.selectedSession = {
277
+ activityPreview: {
278
+ state: 'cancelled',
279
+ statusText: 'Run interrupted: User stopped the current run.',
280
+ },
281
+ status: 'idle',
282
+ };
283
+
284
+ renderArea('session-1');
285
+
286
+ expect(screen.queryByTestId('conversation-bottom-slot')).toBeNull();
287
+ expect(screen.queryByText(/出错了|Something went wrong/)).toBeNull();
273
288
  });
274
289
  });
@@ -497,7 +497,7 @@ it("renders context compaction as an in-flow divider instead of a chat message",
497
497
  updatedAt: "2026-05-05T12:00:00.000Z",
498
498
  },
499
499
  },
500
- parts: [{ type: "text", text: "较早上下文已自动压缩" }],
500
+ parts: [{ type: "text", text: "Earlier context was auto-compacted" }],
501
501
  } satisfies NcpMessage;
502
502
 
503
503
  const { getByText } = render(
@@ -217,7 +217,15 @@ describe("useNcpAgentRuntime", () => {
217
217
  await result.current.abort();
218
218
  });
219
219
 
220
- expect(client.abort).toHaveBeenCalledWith({ sessionId: "session-running" });
220
+ expect(client.abort).toHaveBeenCalledWith({
221
+ sessionId: "session-running",
222
+ runId: undefined,
223
+ reason: {
224
+ code: "abort-error",
225
+ message: "User stopped the current run.",
226
+ details: { source: "chat-ui" },
227
+ },
228
+ });
221
229
  });
222
230
 
223
231
  it("clears the local running state when the live stream publishes message.abort", async () => {
@@ -243,6 +251,20 @@ describe("useNcpAgentRuntime", () => {
243
251
  expect(result.current.activeRunId).toBe("run-1");
244
252
  });
245
253
 
254
+ await act(async () => {
255
+ await result.current.abort();
256
+ });
257
+
258
+ expect(client.abort).toHaveBeenCalledWith({
259
+ sessionId: "session-running",
260
+ runId: "run-1",
261
+ reason: {
262
+ code: "abort-error",
263
+ message: "User stopped the current run.",
264
+ details: { source: "chat-ui" },
265
+ },
266
+ });
267
+
246
268
  await act(async () => {
247
269
  await client.emit({
248
270
  type: NcpEventType.MessageAbort,
@@ -0,0 +1,74 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useNcpSessionListView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
4
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
+ import type { NcpSessionSummaryView } from "@/shared/lib/api";
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ sessions: [] as NcpSessionSummaryView[],
9
+ }));
10
+
11
+ vi.mock("@/features/chat/features/ncp/hooks/use-ncp-session-queries", () => ({
12
+ useNcpSessions: () => ({
13
+ data: {
14
+ sessions: mocks.sessions,
15
+ total: mocks.sessions.length,
16
+ },
17
+ isLoading: false,
18
+ }),
19
+ }));
20
+
21
+ function createSummary(
22
+ sessionId: string,
23
+ label: string,
24
+ ): NcpSessionSummaryView {
25
+ return {
26
+ sessionId,
27
+ status: "idle",
28
+ updatedAt: "2026-06-18T00:00:00.000Z",
29
+ lastMessageAt: "2026-06-18T00:00:00.000Z",
30
+ messageCount: 1,
31
+ metadata: {
32
+ label,
33
+ session_type: "native",
34
+ },
35
+ };
36
+ }
37
+
38
+ describe("useNcpSessionListView", () => {
39
+ beforeEach(() => {
40
+ mocks.sessions = [
41
+ createSummary("session:alpha", "Alpha Task"),
42
+ createSummary("session:beta", "Beta Task"),
43
+ ];
44
+ useChatSessionListStore.setState({
45
+ snapshot: {
46
+ ...useChatSessionListStore.getState().snapshot,
47
+ query: "",
48
+ },
49
+ });
50
+ });
51
+
52
+ it("uses the sidebar query by default", () => {
53
+ useChatSessionListStore.getState().setSnapshot({ query: "Alpha" });
54
+
55
+ const { result } = renderHook(() => useNcpSessionListView());
56
+
57
+ expect(result.current.items.map((item) => item.session.key)).toEqual([
58
+ "session:alpha",
59
+ ]);
60
+ });
61
+
62
+ it("can override the hidden sidebar query for header switching", () => {
63
+ useChatSessionListStore.getState().setSnapshot({ query: "Alpha" });
64
+
65
+ const { result } = renderHook(() =>
66
+ useNcpSessionListView({ query: "" }),
67
+ );
68
+
69
+ expect(result.current.items.map((item) => item.session.key)).toEqual([
70
+ "session:alpha",
71
+ "session:beta",
72
+ ]);
73
+ });
74
+ });
@@ -22,8 +22,9 @@ function shouldShowSessionInSidebar(session: SessionEntryView): boolean {
22
22
  return session.isPromotedChildSession === true;
23
23
  }
24
24
 
25
- export function useNcpSessionListView(params: { limit?: number } = {}) {
26
- const query = useChatSessionListStore((state) => state.snapshot.query);
25
+ export function useNcpSessionListView(params: { limit?: number; query?: string | null } = {}) {
26
+ const storedQuery = useChatSessionListStore((state) => state.snapshot.query);
27
+ const query = params.query ?? storedQuery;
27
28
  const sessionsQuery = useNcpSessions({ limit: params.limit ?? 200 });
28
29
 
29
30
  const items = useMemo<NcpSessionListItemView[]>(() => {