@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
@@ -0,0 +1,238 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { MemoryRouter } from "react-router-dom";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { AgentsPage } from "@/components/agents/agents-page";
6
+ import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
7
+ import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
8
+ import { setLanguage } from "@/lib/i18n";
9
+
10
+ const mocks = vi.hoisted(() => ({
11
+ createAgent: vi.fn(),
12
+ updateAgent: vi.fn(),
13
+ deleteAgent: vi.fn(),
14
+ sessionTypesQuery: {
15
+ data: {
16
+ defaultType: "native",
17
+ options: [
18
+ { value: "native", label: "Native", ready: true },
19
+ { value: "codex", label: "Codex", ready: true },
20
+ {
21
+ value: "claude",
22
+ label: "Claude Code",
23
+ ready: false,
24
+ reasonMessage: "Configure Claude Code first.",
25
+ },
26
+ ],
27
+ },
28
+ },
29
+ agentsQuery: {
30
+ data: {
31
+ agents: [
32
+ {
33
+ id: "main",
34
+ displayName: "Main",
35
+ description: "系统默认入口与总控协作者。",
36
+ builtIn: true,
37
+ model: "openai/gpt-5.1",
38
+ workspace: "~/.nextclaw/workspace",
39
+ avatarUrl: null,
40
+ },
41
+ {
42
+ id: "researcher",
43
+ displayName: "Researcher",
44
+ description: "负责调研、信息筛选与结论提炼。",
45
+ builtIn: false,
46
+ model: "openai/gpt-5.2",
47
+ runtime: "codex",
48
+ workspace: "~/.nextclaw/workspace/agents/researcher",
49
+ avatarUrl: null,
50
+ },
51
+ ],
52
+ },
53
+ isLoading: false,
54
+ },
55
+ configQuery: {
56
+ data: {
57
+ agents: {
58
+ defaults: {
59
+ model: "openai/gpt-5.1",
60
+ workspace: "~/.nextclaw/workspace",
61
+ },
62
+ },
63
+ providers: {
64
+ openai: {
65
+ enabled: true,
66
+ apiKeySet: true,
67
+ models: ["gpt-5.1", "gpt-5.2"],
68
+ },
69
+ },
70
+ },
71
+ },
72
+ configMetaQuery: {
73
+ data: {
74
+ providers: [
75
+ {
76
+ name: "openai",
77
+ displayName: "OpenAI",
78
+ modelPrefix: "openai",
79
+ defaultModels: ["openai/gpt-5.1", "openai/gpt-5.2"],
80
+ keywords: [],
81
+ envKey: "OPENAI_API_KEY",
82
+ },
83
+ ],
84
+ },
85
+ },
86
+ }));
87
+
88
+ vi.mock("@/hooks/agents/useAgents", () => ({
89
+ useAgents: () => mocks.agentsQuery,
90
+ useCreateAgent: () => ({
91
+ mutateAsync: mocks.createAgent,
92
+ isPending: false,
93
+ }),
94
+ useUpdateAgent: () => ({
95
+ mutateAsync: mocks.updateAgent,
96
+ isPending: false,
97
+ }),
98
+ useDeleteAgent: () => ({
99
+ mutate: mocks.deleteAgent,
100
+ isPending: false,
101
+ }),
102
+ }));
103
+
104
+ vi.mock("@/hooks/useConfig", () => ({
105
+ useConfig: () => mocks.configQuery,
106
+ useConfigMeta: () => mocks.configMetaQuery,
107
+ }));
108
+
109
+ vi.mock("@/hooks/use-ncp-chat-session-types", () => ({
110
+ useNcpChatSessionTypes: () => mocks.sessionTypesQuery,
111
+ }));
112
+
113
+ describe("AgentsPage", () => {
114
+ beforeEach(() => {
115
+ setLanguage("zh");
116
+ mocks.createAgent.mockReset();
117
+ mocks.updateAgent.mockReset();
118
+ mocks.deleteAgent.mockReset();
119
+ if (!HTMLElement.prototype.hasPointerCapture) {
120
+ HTMLElement.prototype.hasPointerCapture = () => false;
121
+ }
122
+ if (!HTMLElement.prototype.setPointerCapture) {
123
+ HTMLElement.prototype.setPointerCapture = () => {};
124
+ }
125
+ if (!HTMLElement.prototype.releasePointerCapture) {
126
+ HTMLElement.prototype.releasePointerCapture = () => {};
127
+ }
128
+ useChatInputStore.setState({
129
+ snapshot: {
130
+ ...useChatInputStore.getState().snapshot,
131
+ pendingSessionType: "native",
132
+ pendingProjectRoot: "/tmp/demo-project",
133
+ pendingProjectRootSessionKey: "draft-session",
134
+ },
135
+ });
136
+ useChatSessionListStore.setState({
137
+ snapshot: {
138
+ ...useChatSessionListStore.getState().snapshot,
139
+ selectedAgentId: "main",
140
+ selectedSessionKey: "session-1",
141
+ },
142
+ });
143
+ });
144
+
145
+ it("renders the agents workspace in Chinese and keeps core actions visible", async () => {
146
+ const user = userEvent.setup();
147
+
148
+ render(
149
+ <MemoryRouter>
150
+ <AgentsPage />
151
+ </MemoryRouter>,
152
+ );
153
+
154
+ expect(screen.getByText("Agent 管理台")).toBeTruthy();
155
+ expect(
156
+ screen.getByText("让每个 Agent 都像真正的协作者一样存在"),
157
+ ).toBeTruthy();
158
+ expect(screen.getByText("全部 Agent")).toBeTruthy();
159
+ expect(screen.getAllByText("主目录").length).toBeGreaterThan(0);
160
+ expect(screen.getAllByRole("button", { name: "开始对话" })).toHaveLength(2);
161
+ expect(screen.getAllByRole("button", { name: "编辑" })).toHaveLength(2);
162
+ expect(screen.getByText("负责调研、信息筛选与结论提炼。")).toBeTruthy();
163
+ expect(
164
+ screen.queryByText("专属 Agent 身份,可沉淀自己的记忆、技能与角色风格。"),
165
+ ).toBeNull();
166
+ expect(screen.queryByText("Agent Gallery")).toBeNull();
167
+
168
+ await user.click(screen.getAllByRole("button", { name: "编辑" })[1]);
169
+
170
+ expect(screen.getByText("编辑 Agent 身份")).toBeTruthy();
171
+ expect(screen.getByText("主目录保持不变")).toBeTruthy();
172
+ expect(screen.getByDisplayValue("Researcher")).toBeTruthy();
173
+ expect(
174
+ screen.getByDisplayValue("负责调研、信息筛选与结论提炼。").tagName,
175
+ ).toBe("TEXTAREA");
176
+ expect(screen.getByDisplayValue("gpt-5.2")).toBeTruthy();
177
+ });
178
+
179
+ it("uses a runtime dropdown instead of manual text input when editing an agent", async () => {
180
+ const user = userEvent.setup();
181
+
182
+ render(
183
+ <MemoryRouter>
184
+ <AgentsPage />
185
+ </MemoryRouter>,
186
+ );
187
+
188
+ await user.click(screen.getAllByRole("button", { name: "编辑" })[1]);
189
+
190
+ const runtimeTrigger = screen.getByRole("combobox", { name: "Runtime" });
191
+ expect(
192
+ screen.queryByPlaceholderText("Runtime(如 native 或 codex,可选)"),
193
+ ).toBeNull();
194
+ expect(runtimeTrigger.textContent).toContain("Codex");
195
+ expect(screen.queryByText("跟随默认 Runtime")).toBeNull();
196
+
197
+ await user.click(runtimeTrigger);
198
+ await user.click(screen.getByRole("option", { name: "Codex" }));
199
+ await user.click(screen.getByRole("button", { name: "保存编辑" }));
200
+
201
+ expect(mocks.updateAgent).toHaveBeenCalledWith({
202
+ agentId: "researcher",
203
+ data: {
204
+ displayName: "Researcher",
205
+ description: "负责调研、信息筛选与结论提炼。",
206
+ avatar: "",
207
+ model: "openai/gpt-5.2",
208
+ runtime: "codex",
209
+ },
210
+ });
211
+ });
212
+
213
+ it("starts a draft chat with the agent runtime as the pending session type", async () => {
214
+ const user = userEvent.setup();
215
+
216
+ render(
217
+ <MemoryRouter>
218
+ <AgentsPage />
219
+ </MemoryRouter>,
220
+ );
221
+
222
+ await user.click(screen.getAllByRole("button", { name: "开始对话" })[1]);
223
+
224
+ expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe(
225
+ "researcher",
226
+ );
227
+ expect(
228
+ useChatSessionListStore.getState().snapshot.selectedSessionKey,
229
+ ).toBeNull();
230
+ expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe(
231
+ "codex",
232
+ );
233
+ expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
234
+ expect(
235
+ useChatInputStore.getState().snapshot.pendingProjectRootSessionKey,
236
+ ).toBeNull();
237
+ });
238
+ });
@@ -0,0 +1,435 @@
1
+ import { useMemo, useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import {
4
+ useCreateAgent,
5
+ useDeleteAgent,
6
+ useAgents,
7
+ useUpdateAgent,
8
+ } from "@/hooks/agents/useAgents";
9
+ import { useConfig, useConfigMeta } from "@/hooks/useConfig";
10
+ import type { AgentProfileView } from "@/api/types";
11
+ import {
12
+ AgentCreateDialog,
13
+ AgentEditDialog,
14
+ type AgentCreateFormState,
15
+ type AgentEditFormState,
16
+ } from "@/components/agents/agent-dialogs";
17
+ import {
18
+ buildSessionTypeOptions,
19
+ normalizeSessionType,
20
+ resolveAgentRuntimeSessionType,
21
+ resolveSessionTypeLabel,
22
+ } from "@/components/chat/useChatSessionTypeState";
23
+ import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
24
+ import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
25
+ import { AgentAvatar } from "@/components/common/AgentAvatar";
26
+ import { Button } from "@/components/ui/button";
27
+ import { Card, CardContent } from "@/components/ui/card";
28
+ import { NoticeCard } from "@/components/ui/notice-card";
29
+ import { TagChip } from "@/components/ui/tag-chip";
30
+ import { useNcpChatSessionTypes } from "@/hooks/use-ncp-chat-session-types";
31
+ import { PageLayout } from "@/components/layout/page-layout";
32
+ import { t } from "@/lib/i18n";
33
+ import { buildProviderModelCatalog } from "@/lib/provider-models";
34
+ import { cn } from "@/lib/utils";
35
+ import {
36
+ Bot,
37
+ House,
38
+ MessageCircle,
39
+ Pencil,
40
+ Plus,
41
+ ShieldCheck,
42
+ Sparkles,
43
+ Trash2,
44
+ } from "lucide-react";
45
+
46
+ const CARD_TONES = [
47
+ {
48
+ strip: "bg-[#efc37a]",
49
+ chip: "border-[#f2d7a7] bg-[#fff8eb] text-[#8d5a18]",
50
+ },
51
+ {
52
+ strip: "bg-[#8fd4c0]",
53
+ chip: "border-[#bde6da] bg-[#effbf7] text-[#156653]",
54
+ },
55
+ {
56
+ strip: "bg-[#b7c9fb]",
57
+ chip: "border-[#d7e2ff] bg-[#f4f7ff] text-[#2d4d8f]",
58
+ },
59
+ ] as const;
60
+
61
+ function resolveAgentTone(index: number, builtIn: boolean) {
62
+ if (builtIn) {
63
+ return {
64
+ strip: "bg-[#e6b765]",
65
+ chip: "border-[#f2d19c] bg-[#fff8ec] text-[#90550d]",
66
+ };
67
+ }
68
+ return CARD_TONES[index % CARD_TONES.length];
69
+ }
70
+
71
+ function AgentsHero(props: { agentCount: number; onCreate: () => void }) {
72
+ const { agentCount, onCreate } = props;
73
+
74
+ return (
75
+ <section className="relative overflow-hidden rounded-[28px] border border-[#f0d6aa] bg-[linear-gradient(135deg,#fff7ea_0%,#fff9f1_32%,#f2fbff_100%)] px-5 py-5 sm:px-6">
76
+ <div className="absolute inset-y-0 right-0 w-[46%] bg-[radial-gradient(circle_at_top_right,rgba(255,215,163,0.52),transparent_54%)]" />
77
+ <div className="absolute -bottom-10 left-8 h-32 w-32 rounded-full bg-[#ffe6c0]/55 blur-3xl" />
78
+ <div className="relative grid gap-5 xl:grid-cols-[minmax(0,1fr)_320px] xl:items-center">
79
+ <div className="max-w-3xl space-y-3">
80
+ <div className="inline-flex items-center gap-2 rounded-full border border-white/70 bg-white/80 px-3 py-1 text-[11px] font-semibold tracking-[0.16em] text-[#9b6118]">
81
+ <Sparkles className="h-3.5 w-3.5" />
82
+ {t("agentsHeroEyebrow")}
83
+ </div>
84
+ <div className="space-y-2">
85
+ <h1 className="max-w-2xl text-[30px] font-semibold leading-tight tracking-[-0.05em] text-[#2f2212] sm:text-[38px]">
86
+ {t("agentsHeroTitle")}
87
+ </h1>
88
+ <p className="max-w-2xl text-sm leading-6 text-[#6d5841] sm:text-[15px] sm:leading-7">
89
+ {t("agentsHeroDescription")}
90
+ </p>
91
+ </div>
92
+ <div className="pt-1">
93
+ <div className="inline-flex items-center gap-3 rounded-2xl border border-[#f2d5a4] bg-white/82 px-3 py-2 text-[#7a4d12] shadow-[0_14px_30px_rgba(167,117,47,0.07)]">
94
+ <span className="text-[11px] font-semibold tracking-[0.14em]">
95
+ {t("agentsOverviewTotal")}
96
+ </span>
97
+ <span className="text-xl font-semibold tracking-[-0.04em] text-[#1f2937]">
98
+ {agentCount}
99
+ </span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <div className="flex shrink-0 flex-col gap-3">
104
+ <Button
105
+ type="button"
106
+ variant="primary"
107
+ className="h-10 rounded-2xl px-5 text-sm font-semibold"
108
+ onClick={onCreate}
109
+ >
110
+ <Plus className="mr-2 h-4 w-4" />
111
+ {t("agentsCreateButton")}
112
+ </Button>
113
+ <NoticeCard
114
+ title={t("agentsCreateDialogHint")}
115
+ className="border-white/70 bg-white/72 text-xs leading-6 shadow-[0_18px_40px_rgba(167,117,47,0.08)]"
116
+ />
117
+ </div>
118
+ </div>
119
+ </section>
120
+ );
121
+ }
122
+
123
+ function AgentListCard(props: {
124
+ agent: AgentProfileView;
125
+ index: number;
126
+ runtimeOptions: { value: string; label: string }[];
127
+ defaultRuntimeLabel: string;
128
+ updatePending: boolean;
129
+ deletePending: boolean;
130
+ onStartChat: () => void;
131
+ onEdit: () => void;
132
+ onDelete: () => void;
133
+ }) {
134
+ const {
135
+ agent,
136
+ index,
137
+ runtimeOptions,
138
+ defaultRuntimeLabel,
139
+ updatePending,
140
+ deletePending,
141
+ onStartChat,
142
+ onEdit,
143
+ onDelete,
144
+ } = props;
145
+ const tone = resolveAgentTone(index, Boolean(agent.builtIn));
146
+ const runtimeValue = agent.runtime?.trim() || agent.engine?.trim() || "";
147
+ const runtimeLabel = runtimeValue
148
+ ? (runtimeOptions.find(
149
+ (option) => option.value === normalizeSessionType(runtimeValue),
150
+ )?.label ?? resolveSessionTypeLabel(runtimeValue))
151
+ : defaultRuntimeLabel;
152
+
153
+ return (
154
+ <Card className="overflow-hidden border border-gray-200 bg-white shadow-sm transition-shadow duration-200 hover:shadow-md">
155
+ <div className={cn("h-1.5 w-full", tone.strip)} />
156
+ <CardContent className="flex h-full flex-col gap-4 px-4 py-4">
157
+ <div className="flex items-start gap-3">
158
+ <AgentAvatar
159
+ agentId={agent.id}
160
+ displayName={agent.displayName}
161
+ avatarUrl={agent.avatarUrl}
162
+ className="h-11 w-11 shrink-0"
163
+ />
164
+ <div className="min-w-0 flex-1 space-y-1 pt-0.5">
165
+ <div className="flex flex-wrap items-center gap-2">
166
+ <div className="truncate text-lg font-semibold tracking-[-0.03em] text-[#1f2937]">
167
+ {agent.displayName?.trim() || agent.id}
168
+ </div>
169
+ {agent.builtIn ? (
170
+ <TagChip tone="warning" className={cn("gap-1", tone.chip)}>
171
+ <ShieldCheck className="h-3 w-3" />
172
+ {t("agentsCardBuiltInTag")}
173
+ </TagChip>
174
+ ) : null}
175
+ </div>
176
+ <div className="text-[11px] font-medium uppercase tracking-[0.18em] text-[#94a3b8]">
177
+ @{agent.id}
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <p className="text-sm leading-6 text-[#64748b]">
183
+ {agent.description?.trim() ||
184
+ (agent.builtIn
185
+ ? t("agentsCardBuiltInSummary")
186
+ : t("agentsCardCustomSummary"))}
187
+ </p>
188
+
189
+ <div className="mt-auto flex flex-col gap-4">
190
+ <div>
191
+ <div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
192
+ <Sparkles className="h-3.5 w-3.5" />
193
+ {t("agentsCardRuntimeLabel")}
194
+ </div>
195
+ <div className="mt-1.5 text-sm leading-6 text-[#475569]">
196
+ {runtimeLabel}
197
+ </div>
198
+ </div>
199
+
200
+ <div className="border-t border-gray-100 pt-3">
201
+ <div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
202
+ <House className="h-3.5 w-3.5" />
203
+ {t("agentsCardHomeLabel")}
204
+ </div>
205
+ <div className="mt-1.5 break-all text-sm leading-6 text-[#475569]">
206
+ {agent.workspace ?? "-"}
207
+ </div>
208
+ </div>
209
+
210
+ <div className="flex flex-wrap items-center gap-2">
211
+ <Button
212
+ type="button"
213
+ variant="primary"
214
+ className="h-9 rounded-xl px-4"
215
+ onClick={onStartChat}
216
+ >
217
+ <MessageCircle className="mr-2 h-4 w-4" />
218
+ {t("agentsCardStartChat")}
219
+ </Button>
220
+ <Button
221
+ type="button"
222
+ variant="ghost"
223
+ className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
224
+ onClick={onEdit}
225
+ disabled={updatePending}
226
+ >
227
+ <Pencil className="mr-1.5 h-3.5 w-3.5" />
228
+ {t("agentsEditAction")}
229
+ </Button>
230
+ {!agent.builtIn ? (
231
+ <Button
232
+ type="button"
233
+ variant="ghost"
234
+ className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
235
+ onClick={onDelete}
236
+ disabled={deletePending}
237
+ >
238
+ <Trash2 className="mr-1.5 h-3.5 w-3.5" />
239
+ {t("agentsRemoveAction")}
240
+ </Button>
241
+ ) : null}
242
+ </div>
243
+ </div>
244
+ </CardContent>
245
+ </Card>
246
+ );
247
+ }
248
+
249
+ export function AgentsPage() {
250
+ const navigate = useNavigate();
251
+ const agentsQuery = useAgents();
252
+ const configQuery = useConfig();
253
+ const configMetaQuery = useConfigMeta();
254
+ const sessionTypesQuery = useNcpChatSessionTypes();
255
+ const createAgent = useCreateAgent();
256
+ const updateAgent = useUpdateAgent();
257
+ const deleteAgent = useDeleteAgent();
258
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
259
+ const [editingAgent, setEditingAgent] = useState<AgentProfileView | null>(
260
+ null,
261
+ );
262
+ const setSessionListSnapshot = useChatSessionListStore(
263
+ (state) => state.setSnapshot,
264
+ );
265
+
266
+ const agents = useMemo(
267
+ () => agentsQuery.data?.agents ?? [],
268
+ [agentsQuery.data?.agents],
269
+ );
270
+ const sortedAgents = useMemo(
271
+ () =>
272
+ [...agents].sort(
273
+ (left, right) =>
274
+ Number(Boolean(right.builtIn)) - Number(Boolean(left.builtIn)) ||
275
+ left.id.localeCompare(right.id),
276
+ ),
277
+ [agents],
278
+ );
279
+ const providerCatalog = useMemo(
280
+ () =>
281
+ buildProviderModelCatalog({
282
+ config: configQuery.data,
283
+ meta: configMetaQuery.data,
284
+ onlyConfigured: true,
285
+ }),
286
+ [configMetaQuery.data, configQuery.data],
287
+ );
288
+ const runtimeOptions = useMemo(
289
+ () => buildSessionTypeOptions(sessionTypesQuery.data?.options ?? []),
290
+ [sessionTypesQuery.data?.options],
291
+ );
292
+ const defaultRuntime = useMemo(
293
+ () => normalizeSessionType(sessionTypesQuery.data?.defaultType ?? "native"),
294
+ [sessionTypesQuery.data?.defaultType],
295
+ );
296
+ const defaultRuntimeLabel = useMemo(
297
+ () =>
298
+ runtimeOptions.find((option) => option.value === defaultRuntime)?.label ??
299
+ resolveSessionTypeLabel(defaultRuntime),
300
+ [defaultRuntime, runtimeOptions],
301
+ );
302
+
303
+ const handleCreate = async (form: AgentCreateFormState) => {
304
+ await createAgent.mutateAsync({
305
+ data: {
306
+ id: form.id,
307
+ ...(form.displayName.trim()
308
+ ? { displayName: form.displayName.trim() }
309
+ : {}),
310
+ ...(form.description.trim()
311
+ ? { description: form.description.trim() }
312
+ : {}),
313
+ ...(form.avatar.trim() ? { avatar: form.avatar.trim() } : {}),
314
+ ...(form.home.trim() ? { home: form.home.trim() } : {}),
315
+ ...(form.model.trim() ? { model: form.model.trim() } : {}),
316
+ ...(form.runtime.trim() ? { runtime: form.runtime.trim() } : {}),
317
+ },
318
+ });
319
+ setIsCreateDialogOpen(false);
320
+ };
321
+
322
+ const handleStartEdit = (agent: AgentProfileView) => {
323
+ setEditingAgent(agent);
324
+ };
325
+
326
+ const handleUpdate = async (agentId: string, form: AgentEditFormState) => {
327
+ await updateAgent.mutateAsync({
328
+ agentId,
329
+ data: {
330
+ displayName: form.displayName,
331
+ description: form.description,
332
+ avatar: form.avatar,
333
+ model: form.model,
334
+ ...(form.runtime.trim()
335
+ ? { runtime: form.runtime.trim() }
336
+ : { runtime: "" }),
337
+ },
338
+ });
339
+ setEditingAgent(null);
340
+ };
341
+
342
+ const startChatWithAgent = (agent: AgentProfileView) => {
343
+ setSessionListSnapshot({
344
+ selectedAgentId: agent.id,
345
+ selectedSessionKey: null,
346
+ });
347
+ useChatInputStore.getState().setSnapshot({
348
+ pendingSessionType: resolveAgentRuntimeSessionType(agent, defaultRuntime),
349
+ pendingProjectRoot: null,
350
+ pendingProjectRootSessionKey: null,
351
+ });
352
+ navigate("/chat");
353
+ };
354
+
355
+ return (
356
+ <PageLayout className="space-y-5">
357
+ <AgentsHero
358
+ agentCount={agents.length}
359
+ onCreate={() => setIsCreateDialogOpen(true)}
360
+ />
361
+
362
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
363
+ {agentsQuery.isLoading ? (
364
+ <Card className="md:col-span-2 xl:col-span-3 border-dashed border-[#d9dce3] bg-white/70">
365
+ <CardContent className="py-14 text-center text-sm text-gray-500">
366
+ {t("agentsLoading")}
367
+ </CardContent>
368
+ </Card>
369
+ ) : sortedAgents.length === 0 ? (
370
+ <Card className="md:col-span-2 xl:col-span-3 overflow-hidden border-dashed border-[#d9dce3] bg-[linear-gradient(135deg,#fff7ea_0%,#f4fbff_100%)]">
371
+ <CardContent className="flex min-h-[240px] flex-col items-center justify-center px-6 py-14 text-center">
372
+ <div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white/80 shadow-[0_18px_44px_rgba(0,0,0,0.08)]">
373
+ <Bot className="h-8 w-8 text-[#d39a3b]" />
374
+ </div>
375
+ <div className="text-lg font-semibold text-[#2f2212]">
376
+ {t("agentsEmpty")}
377
+ </div>
378
+ <p className="mt-2 max-w-md text-sm leading-6 text-[#78644d]">
379
+ {t("agentsEmptyDescription")}
380
+ </p>
381
+ <Button
382
+ type="button"
383
+ variant="primary"
384
+ className="mt-5 rounded-2xl px-5"
385
+ onClick={() => setIsCreateDialogOpen(true)}
386
+ >
387
+ <Plus className="mr-2 h-4 w-4" />
388
+ {t("agentsCreateButton")}
389
+ </Button>
390
+ </CardContent>
391
+ </Card>
392
+ ) : (
393
+ sortedAgents.map((agent, index) => (
394
+ <AgentListCard
395
+ key={agent.id}
396
+ agent={agent}
397
+ index={index}
398
+ runtimeOptions={runtimeOptions}
399
+ defaultRuntimeLabel={defaultRuntimeLabel}
400
+ updatePending={updateAgent.isPending}
401
+ deletePending={deleteAgent.isPending}
402
+ onStartChat={() => startChatWithAgent(agent)}
403
+ onEdit={() => handleStartEdit(agent)}
404
+ onDelete={() => deleteAgent.mutate({ agentId: agent.id })}
405
+ />
406
+ ))
407
+ )}
408
+ </div>
409
+
410
+ <AgentCreateDialog
411
+ open={isCreateDialogOpen}
412
+ pending={createAgent.isPending}
413
+ providerCatalog={providerCatalog}
414
+ runtimeOptions={runtimeOptions}
415
+ defaultRuntime={defaultRuntime}
416
+ onOpenChange={setIsCreateDialogOpen}
417
+ onSubmit={handleCreate}
418
+ />
419
+
420
+ <AgentEditDialog
421
+ agent={editingAgent}
422
+ pending={updateAgent.isPending}
423
+ providerCatalog={providerCatalog}
424
+ runtimeOptions={runtimeOptions}
425
+ defaultRuntime={defaultRuntime}
426
+ onOpenChange={(open) => {
427
+ if (!open && !updateAgent.isPending) {
428
+ setEditingAgent(null);
429
+ }
430
+ }}
431
+ onSubmit={handleUpdate}
432
+ />
433
+ </PageLayout>
434
+ );
435
+ }