@nextclaw/ui 0.12.9 → 0.12.10

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 (178) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-Da7khnBA.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-D281Rotl.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-VnFElj4E.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-BdcxxoQu.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-_i4MU2bB.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-DtIQwrHF.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-ccBFUi-o.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-6TrrnPCR.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/{download-BhDxnyvU.js → download-BD0ETkB-.js} +1 -1
  27. package/dist/assets/{external-link-BgErLCNT.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-Bl7dr_UG.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/{i18n-eDHeDY0n.js → i18n-CpTZLchQ.js} +1 -1
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-ZDS92Qpp.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-x89HbrZ4.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-vZnghcFy.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-DT98i__E.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-JtFzpNn6.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-3S6-H3Xw.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-vbanNPFU.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-BsLtHOH-.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-G48scll7.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-DkNhD-42.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-BkvTN-vd.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +22 -22
  61. package/dist/runtime-icons/claude.ico +0 -0
  62. package/dist/runtime-icons/codex-openai.svg +6 -0
  63. package/dist/runtime-icons/hermes-agent.png +0 -0
  64. package/package.json +6 -6
  65. package/public/runtime-icons/claude.ico +0 -0
  66. package/public/runtime-icons/codex-openai.svg +6 -0
  67. package/public/runtime-icons/hermes-agent.png +0 -0
  68. package/src/account/components/account-panel.tsx +217 -97
  69. package/src/account/managers/account.manager.ts +3 -2
  70. package/src/api/chat-session-type.types.ts +7 -0
  71. package/src/api/runtime-control.types.ts +8 -0
  72. package/src/api/types.ts +8 -0
  73. package/src/app.tsx +221 -57
  74. package/src/components/agents/agent-dialogs.tsx +499 -0
  75. package/src/components/agents/agents-page.test.tsx +238 -0
  76. package/src/components/agents/agents-page.tsx +435 -0
  77. package/src/components/chat/ChatSidebar.tsx +11 -35
  78. package/src/components/chat/chat-conversation-panel.test.tsx +20 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-page-shell.tsx +19 -13
  81. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  82. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  84. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  85. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  86. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  87. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  88. package/src/components/chat/ncp/ncp-chat-page.tsx +2 -0
  89. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  90. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  91. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  92. package/src/components/chat/stores/chat-input.store.ts +2 -1
  93. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  94. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  95. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  96. package/src/components/common/BrandHeader.tsx +3 -1
  97. package/src/components/common/session-context-icon.tsx +15 -2
  98. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  99. package/src/components/config/ChannelForm.test.tsx +89 -3
  100. package/src/components/config/ChannelForm.tsx +157 -188
  101. package/src/components/config/ChannelsList.test.tsx +163 -119
  102. package/src/components/config/ChannelsList.tsx +90 -101
  103. package/src/components/config/ProviderForm.tsx +108 -146
  104. package/src/components/config/ProvidersList.tsx +100 -123
  105. package/src/components/config/SearchConfig.tsx +423 -393
  106. package/src/components/config/channel-form-fields-section.tsx +70 -37
  107. package/src/components/config/config-split-page.tsx +109 -0
  108. package/src/components/config/provider-enabled-field.tsx +17 -10
  109. package/src/components/config/runtime-control-card.test.tsx +56 -0
  110. package/src/components/config/runtime-control-card.tsx +25 -0
  111. package/src/components/config/runtime-presence-card.tsx +93 -79
  112. package/src/components/layout/AppLayout.tsx +25 -37
  113. package/src/components/layout/app-layout.test.tsx +46 -14
  114. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  115. package/src/components/layout/runtime-status-entry.tsx +143 -0
  116. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  117. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  118. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  119. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  120. package/src/components/marketplace/marketplace-page.tsx +596 -0
  121. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  122. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  123. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  124. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  125. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  126. package/src/components/remote/remote-access-page.test.tsx +105 -0
  127. package/src/components/remote/remote-access-page.tsx +248 -0
  128. package/src/components/ui/notice-card.tsx +129 -0
  129. package/src/components/ui/setting-row.tsx +51 -0
  130. package/src/components/ui/tag-chip.tsx +39 -0
  131. package/src/components/ui/textarea.tsx +19 -0
  132. package/src/hooks/useConfig.ts +2 -1
  133. package/src/index.css +24 -0
  134. package/src/lib/app-resource-uri.test.ts +20 -0
  135. package/src/lib/app-resource-uri.ts +29 -0
  136. package/src/lib/i18n.remote.ts +1 -1
  137. package/src/lib/i18n.runtime-control.ts +31 -0
  138. package/src/lib/i18n.ts +5 -8
  139. package/src/lib/session-context.utils.test.ts +71 -0
  140. package/src/lib/session-context.utils.ts +28 -3
  141. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  142. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  143. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  144. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  145. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  146. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  147. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  148. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  149. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  150. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  151. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  152. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  153. package/dist/assets/SessionsConfig-ChHQ7M5c.js +0 -2
  154. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  155. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  156. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  157. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  158. package/dist/assets/index-CF9xve0E.js +0 -6
  159. package/dist/assets/index-FgA52VBt.css +0 -1
  160. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  161. package/dist/assets/play-CFUwCA2E.js +0 -1
  162. package/dist/assets/plus-rYsv72JG.js +0 -1
  163. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  164. package/dist/assets/search-3kFR_zh9.js +0 -1
  165. package/dist/assets/security-config-BWaiARNk.js +0 -1
  166. package/dist/assets/select-DJ2MUjBB.js +0 -41
  167. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  168. package/dist/assets/x-ByDbItbq.js +0 -1
  169. package/src/components/agents/AgentDialogs.tsx +0 -400
  170. package/src/components/agents/AgentsPage.test.tsx +0 -217
  171. package/src/components/agents/AgentsPage.tsx +0 -352
  172. package/src/components/config/config-layout.ts +0 -10
  173. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  174. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  175. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  176. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  177. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  178. package/src/components/remote/RemoteAccessPage.tsx +0 -144
@@ -1,7 +1,7 @@
1
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
- import userEvent from '@testing-library/user-event';
3
- import type * as ReactQueryModule from '@tanstack/react-query';
4
- import { ChannelsList } from '@/components/config/ChannelsList';
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import type * as ReactQueryModule from "@tanstack/react-query";
4
+ import { ChannelsList } from "@/components/config/ChannelsList";
5
5
 
6
6
  const mocks = vi.hoisted(() => ({
7
7
  updateChannelMutate: vi.fn(),
@@ -13,88 +13,90 @@ const mocks = vi.hoisted(() => ({
13
13
  channels: {
14
14
  weixin: {
15
15
  enabled: false,
16
- defaultAccountId: '1344b2b24720@im.bot',
17
- baseUrl: 'https://ilinkai.weixin.qq.com',
16
+ defaultAccountId: "1344b2b24720@im.bot",
17
+ baseUrl: "https://ilinkai.weixin.qq.com",
18
18
  pollTimeoutMs: 35000,
19
- allowFrom: ['o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat'],
19
+ allowFrom: ["o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat"],
20
20
  accounts: {
21
- '1344b2b24720@im.bot': {
22
- enabled: true
23
- }
24
- }
25
- }
26
- }
21
+ "1344b2b24720@im.bot": {
22
+ enabled: true,
23
+ },
24
+ },
25
+ },
26
+ },
27
27
  },
28
- isLoading: false
28
+ isLoading: false,
29
29
  },
30
30
  metaQuery: {
31
31
  data: {
32
32
  channels: [
33
33
  {
34
- name: 'weixin',
35
- displayName: 'Weixin',
36
- enabled: false
37
- }
38
- ]
39
- }
34
+ name: "weixin",
35
+ displayName: "Weixin",
36
+ enabled: false,
37
+ },
38
+ ],
39
+ },
40
40
  },
41
41
  schemaQuery: {
42
42
  data: {
43
43
  uiHints: {
44
- 'channels.weixin': {
45
- label: 'Weixin',
46
- help: 'Weixin QR login + getupdates long-poll channel'
44
+ "channels.weixin": {
45
+ label: "Weixin",
46
+ help: "Weixin QR login + getupdates long-poll channel",
47
+ },
48
+ "channels.weixin.baseUrl": {
49
+ label: "API Base URL",
47
50
  },
48
- 'channels.weixin.baseUrl': {
49
- label: 'API Base URL'
50
- }
51
51
  },
52
- actions: []
53
- }
54
- }
52
+ actions: [],
53
+ },
54
+ },
55
55
  }));
56
56
 
57
- vi.mock('qrcode', () => ({
58
- toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,weixin-qr')
57
+ vi.mock("qrcode", () => ({
58
+ toDataURL: vi.fn().mockResolvedValue("data:image/png;base64,weixin-qr"),
59
59
  }));
60
60
 
61
- vi.mock('@tanstack/react-query', async () => {
62
- const actual = await vi.importActual<typeof ReactQueryModule>('@tanstack/react-query');
61
+ vi.mock("@tanstack/react-query", async () => {
62
+ const actual = await vi.importActual<typeof ReactQueryModule>(
63
+ "@tanstack/react-query",
64
+ );
63
65
  return {
64
66
  ...actual,
65
67
  useQueryClient: () => ({
66
- invalidateQueries: vi.fn().mockResolvedValue(undefined)
67
- })
68
+ invalidateQueries: vi.fn().mockResolvedValue(undefined),
69
+ }),
68
70
  };
69
71
  });
70
72
 
71
- vi.mock('@/hooks/useConfig', () => ({
73
+ vi.mock("@/hooks/useConfig", () => ({
72
74
  useConfig: () => mocks.configQuery,
73
75
  useConfigMeta: () => mocks.metaQuery,
74
76
  useConfigSchema: () => mocks.schemaQuery,
75
77
  useUpdateChannel: () => ({
76
78
  mutate: mocks.updateChannelMutate,
77
79
  mutateAsync: mocks.updateChannelMutateAsync,
78
- isPending: false
80
+ isPending: false,
79
81
  }),
80
82
  useExecuteConfigAction: () => ({
81
83
  mutateAsync: vi.fn(),
82
- isPending: false
83
- })
84
+ isPending: false,
85
+ }),
84
86
  }));
85
87
 
86
- vi.mock('@/hooks/use-channel-auth', () => ({
88
+ vi.mock("@/hooks/use-channel-auth", () => ({
87
89
  useStartChannelAuth: () => ({
88
90
  mutateAsync: mocks.startChannelAuthMutateAsync,
89
- isPending: false
91
+ isPending: false,
90
92
  }),
91
93
  usePollChannelAuth: () => ({
92
94
  mutateAsync: mocks.pollChannelAuthMutateAsync,
93
- isPending: false
94
- })
95
+ isPending: false,
96
+ }),
95
97
  }));
96
98
 
97
- describe('ChannelsList', () => {
99
+ describe("ChannelsList", () => {
98
100
  beforeEach(() => {
99
101
  mocks.updateChannelMutate.mockReset();
100
102
  mocks.updateChannelMutateAsync.mockReset();
@@ -104,68 +106,76 @@ describe('ChannelsList', () => {
104
106
  channels: {
105
107
  weixin: {
106
108
  enabled: false,
107
- defaultAccountId: '1344b2b24720@im.bot',
108
- baseUrl: 'https://ilinkai.weixin.qq.com',
109
+ defaultAccountId: "1344b2b24720@im.bot",
110
+ baseUrl: "https://ilinkai.weixin.qq.com",
109
111
  pollTimeoutMs: 35000,
110
- allowFrom: ['o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat'],
112
+ allowFrom: ["o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat"],
111
113
  accounts: {
112
- '1344b2b24720@im.bot': {
113
- enabled: true
114
- }
115
- }
116
- }
117
- }
114
+ "1344b2b24720@im.bot": {
115
+ enabled: true,
116
+ },
117
+ },
118
+ },
119
+ },
118
120
  };
119
121
  mocks.metaQuery.data = {
120
122
  channels: [
121
123
  {
122
- name: 'weixin',
123
- displayName: 'Weixin',
124
- enabled: false
125
- }
126
- ]
124
+ name: "weixin",
125
+ displayName: "Weixin",
126
+ enabled: false,
127
+ },
128
+ ],
127
129
  };
128
130
  });
129
131
 
130
- it('renders weixin qr auth card and starts channel auth', async () => {
132
+ it("renders weixin qr auth card and starts channel auth", async () => {
131
133
  const user = userEvent.setup();
132
134
  mocks.startChannelAuthMutateAsync.mockResolvedValue({
133
- channel: 'weixin',
134
- kind: 'qr_code',
135
- sessionId: 'session-1',
136
- qrCode: 'qr-token',
137
- qrCodeUrl: 'https://example.com/weixin-qr.png',
138
- expiresAt: '2026-03-23T10:00:00.000Z',
135
+ channel: "weixin",
136
+ kind: "qr_code",
137
+ sessionId: "session-1",
138
+ qrCode: "qr-token",
139
+ qrCodeUrl: "https://example.com/weixin-qr.png",
140
+ expiresAt: "2026-03-23T10:00:00.000Z",
139
141
  intervalMs: 60_000,
140
- note: '请扫码'
142
+ note: "请扫码",
141
143
  });
142
144
 
143
145
  render(<ChannelsList />);
144
146
 
145
- await user.click(await screen.findByRole('button', { name: /All Channels/i }));
147
+ await user.click(
148
+ await screen.findByRole("button", { name: /All Channels/i }),
149
+ );
146
150
 
147
- expect((await screen.findAllByText('Weixin')).length).toBeGreaterThan(0);
148
- expect(await screen.findByRole('button', { name: 'Reconnect with QR' })).toBeTruthy();
149
- expect(screen.getByText('Weixin now uses QR login as the primary setup flow.')).toBeTruthy();
151
+ expect((await screen.findAllByText("Weixin")).length).toBeGreaterThan(0);
152
+ expect(
153
+ await screen.findByRole("button", { name: "Reconnect with QR" }),
154
+ ).toBeTruthy();
155
+ expect(
156
+ screen.getByText("Weixin now uses QR login as the primary setup flow."),
157
+ ).toBeTruthy();
150
158
 
151
- await user.click(screen.getByRole('button', { name: 'Reconnect with QR' }));
159
+ await user.click(screen.getByRole("button", { name: "Reconnect with QR" }));
152
160
 
153
161
  await waitFor(() => {
154
162
  expect(mocks.startChannelAuthMutateAsync).toHaveBeenCalledWith({
155
- channel: 'weixin',
163
+ channel: "weixin",
156
164
  data: expect.objectContaining({
157
- accountId: '1344b2b24720@im.bot',
158
- baseUrl: 'https://ilinkai.weixin.qq.com'
159
- })
165
+ accountId: "1344b2b24720@im.bot",
166
+ baseUrl: "https://ilinkai.weixin.qq.com",
167
+ }),
160
168
  });
161
169
  });
162
170
 
163
171
  await waitFor(() => {
164
- expect(screen.getByAltText('Weixin login QR code').getAttribute('src')).toBe('data:image/png;base64,weixin-qr');
172
+ expect(
173
+ screen.getByAltText("Weixin login QR code").getAttribute("src"),
174
+ ).toBe("data:image/png;base64,weixin-qr");
165
175
  });
166
176
  });
167
177
 
168
- it('keeps Weixin, Feishu, Discord, and QQ at the front of the channel list', async () => {
178
+ it("keeps Weixin, Feishu, Discord, and QQ at the front of the channel list", async () => {
169
179
  const user = userEvent.setup();
170
180
  mocks.configQuery.data = {
171
181
  channels: {
@@ -173,92 +183,126 @@ describe('ChannelsList', () => {
173
183
  qq: { enabled: false },
174
184
  discord: { enabled: false },
175
185
  weixin: { enabled: false },
176
- feishu: { enabled: false }
177
- }
186
+ feishu: { enabled: false },
187
+ },
178
188
  } as unknown as typeof mocks.configQuery.data;
179
189
  mocks.metaQuery.data = {
180
190
  channels: [
181
- { name: 'telegram', displayName: 'Telegram', enabled: false },
182
- { name: 'qq', displayName: 'QQ', enabled: false },
183
- { name: 'discord', displayName: 'Discord', enabled: false },
184
- { name: 'weixin', displayName: 'Weixin', enabled: false },
185
- { name: 'feishu', displayName: 'Feishu', enabled: false }
186
- ]
191
+ { name: "telegram", displayName: "Telegram", enabled: false },
192
+ { name: "qq", displayName: "QQ", enabled: false },
193
+ { name: "discord", displayName: "Discord", enabled: false },
194
+ { name: "weixin", displayName: "Weixin", enabled: false },
195
+ { name: "feishu", displayName: "Feishu", enabled: false },
196
+ ],
187
197
  } as typeof mocks.metaQuery.data;
188
198
 
189
199
  const { container } = render(<ChannelsList />);
190
200
 
191
- await user.click(await screen.findByRole('button', { name: /All Channels/i }));
201
+ await user.click(
202
+ await screen.findByRole("button", { name: /All Channels/i }),
203
+ );
192
204
 
193
- const sidebarSection = container.querySelector('section');
205
+ const sidebarSection = container.querySelector("section");
194
206
  if (!(sidebarSection instanceof HTMLElement)) {
195
- throw new Error('channel sidebar not found');
207
+ throw new Error("channel sidebar not found");
196
208
  }
197
209
 
198
- const channelButtons = Array.from(sidebarSection.querySelectorAll('button[type="button"]')).filter((button) => (
199
- ['Weixin', 'Feishu', 'Discord', 'QQ', 'Telegram'].some((label) => button.textContent?.includes(label))
200
- ));
210
+ const channelButtons = Array.from(
211
+ sidebarSection.querySelectorAll('button[type="button"]'),
212
+ ).filter((button) =>
213
+ ["Weixin", "Feishu", "Discord", "QQ", "Telegram"].some((label) =>
214
+ button.textContent?.includes(label),
215
+ ),
216
+ );
201
217
 
202
218
  expect(channelButtons.map((button) => button.textContent)).toEqual([
203
- expect.stringContaining('Weixin'),
204
- expect.stringContaining('Feishu'),
205
- expect.stringContaining('Discord'),
206
- expect.stringContaining('QQ'),
207
- expect.stringContaining('Telegram')
219
+ expect.stringContaining("Weixin"),
220
+ expect.stringContaining("Feishu"),
221
+ expect.stringContaining("Discord"),
222
+ expect.stringContaining("QQ"),
223
+ expect.stringContaining("Telegram"),
208
224
  ]);
209
225
  });
210
226
 
211
- it('saves weixin advanced settings from the advanced section', async () => {
227
+ it("only enables internal scroll chaining on desktop-sized panes", async () => {
228
+ const user = userEvent.setup();
229
+ const { container } = render(<ChannelsList />);
230
+
231
+ await user.click(
232
+ await screen.findByRole("button", { name: /All Channels/i }),
233
+ );
234
+
235
+ const scrollRegions = Array.from(container.querySelectorAll("div")).filter(
236
+ (node) => {
237
+ const classTokens = node.className.split(/\s+/).filter(Boolean);
238
+ return classTokens.includes("xl:overflow-y-auto");
239
+ },
240
+ );
241
+
242
+ expect(scrollRegions.length).toBeGreaterThanOrEqual(2);
243
+ scrollRegions.forEach((region) => {
244
+ const classTokens = region.className.split(/\s+/).filter(Boolean);
245
+ expect(classTokens).toContain("xl:overscroll-contain");
246
+ expect(classTokens).not.toContain("overflow-y-auto");
247
+ expect(classTokens).not.toContain("overscroll-contain");
248
+ });
249
+ });
250
+
251
+ it("saves weixin advanced settings from the advanced section", async () => {
212
252
  const user = userEvent.setup();
213
253
 
214
254
  const { container } = render(<ChannelsList />);
215
255
 
216
- await user.click(await screen.findByRole('button', { name: /All Channels/i }));
217
- await user.click(await screen.findByText('Advanced settings'));
256
+ await user.click(
257
+ await screen.findByRole("button", { name: /All Channels/i }),
258
+ );
259
+ await user.click(await screen.findByText("Advanced settings"));
218
260
 
219
- const timeoutInput = await screen.findByLabelText('Long Poll Timeout (ms)');
261
+ const timeoutInput = await screen.findByLabelText("Long Poll Timeout (ms)");
220
262
  await user.clear(timeoutInput);
221
- await user.type(timeoutInput, '45000');
263
+ await user.type(timeoutInput, "45000");
222
264
 
223
- const accountsJson = container.querySelector('textarea#accounts') as HTMLTextAreaElement | null;
265
+ const accountsJson = container.querySelector(
266
+ "textarea#accounts",
267
+ ) as HTMLTextAreaElement | null;
224
268
  expect(accountsJson).toBeTruthy();
225
269
  if (!accountsJson) {
226
- throw new Error('accounts textarea not found');
270
+ throw new Error("accounts textarea not found");
227
271
  }
228
272
  await user.clear(accountsJson);
229
273
  fireEvent.change(accountsJson, {
230
274
  target: {
231
275
  value: JSON.stringify(
232
276
  {
233
- '1344b2b24720@im.bot': {
277
+ "1344b2b24720@im.bot": {
234
278
  enabled: true,
235
- baseUrl: 'https://ilinkai.weixin.qq.com'
236
- }
279
+ baseUrl: "https://ilinkai.weixin.qq.com",
280
+ },
237
281
  },
238
282
  null,
239
- 2
240
- )
241
- }
283
+ 2,
284
+ ),
285
+ },
242
286
  });
243
287
 
244
- await user.click(screen.getByRole('button', { name: /save/i }));
288
+ await user.click(screen.getByRole("button", { name: /save/i }));
245
289
 
246
290
  await waitFor(() => {
247
291
  expect(mocks.updateChannelMutate).toHaveBeenCalledWith({
248
- channel: 'weixin',
292
+ channel: "weixin",
249
293
  data: expect.objectContaining({
250
294
  enabled: false,
251
- defaultAccountId: '1344b2b24720@im.bot',
252
- baseUrl: 'https://ilinkai.weixin.qq.com',
295
+ defaultAccountId: "1344b2b24720@im.bot",
296
+ baseUrl: "https://ilinkai.weixin.qq.com",
253
297
  pollTimeoutMs: 45000,
254
- allowFrom: ['o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat'],
298
+ allowFrom: ["o9cq804svxfyCCTIqzddDqRBeMC0@im.wechat"],
255
299
  accounts: {
256
- '1344b2b24720@im.bot': {
300
+ "1344b2b24720@im.bot": {
257
301
  enabled: true,
258
- baseUrl: 'https://ilinkai.weixin.qq.com'
259
- }
260
- }
261
- })
302
+ baseUrl: "https://ilinkai.weixin.qq.com",
303
+ },
304
+ },
305
+ }),
262
306
  });
263
307
  });
264
308
  });