@nextclaw/ui 0.11.13 → 0.11.15

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 (38) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/{ChannelsList-BlQD1VuM.js → ChannelsList-WmCqjuMG.js} +1 -1
  3. package/dist/assets/ChatPage-vfnUvEdN.js +37 -0
  4. package/dist/assets/{DocBrowser-DTww3NZc.js → DocBrowser-Cuds8S4N.js} +1 -1
  5. package/dist/assets/{LogoBadge-D0ogG1ut.js → LogoBadge-CiukNh5R.js} +1 -1
  6. package/dist/assets/{MarketplacePage-DTHw6n0X.js → MarketplacePage--cCya2vU.js} +1 -1
  7. package/dist/assets/{McpMarketplacePage-BikE0mBl.js → McpMarketplacePage-SYy23E4x.js} +1 -1
  8. package/dist/assets/{ModelConfig-CvM__Pz1.js → ModelConfig-D4Nd4HWX.js} +1 -1
  9. package/dist/assets/{ProvidersList-DtZWZlL0.js → ProvidersList-k95kUPqD.js} +1 -1
  10. package/dist/assets/{RemoteAccessPage-E5fT1pem.js → RemoteAccessPage-XjDtIQV4.js} +1 -1
  11. package/dist/assets/{RuntimeConfig-DyZNiqYT.js → RuntimeConfig-BR4oBxQ-.js} +1 -1
  12. package/dist/assets/{SearchConfig-C1bhOCNX.js → SearchConfig-fJjFykmH.js} +1 -1
  13. package/dist/assets/{SecretsConfig-CYmy1Sqy.js → SecretsConfig-D7CxxH1q.js} +1 -1
  14. package/dist/assets/{SessionsConfig-DSlhPpIE.js → SessionsConfig-2hYobMkj.js} +1 -1
  15. package/dist/assets/{chat-session-display-D9YuDGe3.js → chat-session-display-BeoRHq9x.js} +1 -1
  16. package/dist/assets/index-DWrpxFk0.js +8 -0
  17. package/dist/assets/{label-C7Xd_hqz.js → label-C2uGecRa.js} +1 -1
  18. package/dist/assets/{page-layout-VxCaUcrD.js → page-layout-DICppNFk.js} +1 -1
  19. package/dist/assets/{popover-CC4znqAM.js → popover-DoacXI3f.js} +1 -1
  20. package/dist/assets/{security-config-7eVxJq8b.js → security-config-54LcQtst.js} +1 -1
  21. package/dist/assets/{skeleton-DhZRDdHm.js → skeleton-DjjQR4NY.js} +1 -1
  22. package/dist/assets/{status-dot-Bi7Ze-LS.js → status-dot-OjV8JCSV.js} +1 -1
  23. package/dist/assets/{switch-COBEivEX.js → switch-Dvd2FqUB.js} +1 -1
  24. package/dist/assets/{tabs-custom-B9j40wuu.js → tabs-custom-DlwuO0Gw.js} +1 -1
  25. package/dist/assets/{useConfirmDialog-N8nuxOq-.js → useConfirmDialog-BuB9l_2r.js} +1 -1
  26. package/dist/index.html +1 -1
  27. package/package.json +5 -5
  28. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +39 -0
  29. package/src/components/chat/adapters/chat-input-bar.adapter.ts +4 -1
  30. package/src/components/chat/adapters/chat-message.adapter.test.ts +40 -0
  31. package/src/components/chat/adapters/chat-message.adapter.ts +11 -0
  32. package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +125 -0
  33. package/src/components/chat/containers/chat-input-bar.container.tsx +8 -10
  34. package/src/components/chat/ncp/NcpChatPage.tsx +18 -24
  35. package/src/components/config/README.md +1 -1
  36. package/src/lib/i18n.ts +0 -2
  37. package/dist/assets/ChatPage-DBvm558n.js +0 -37
  38. package/dist/assets/index-BBz4mi7g.js +0 -8
package/dist/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw</title>
9
- <script type="module" crossorigin src="/assets/index-BBz4mi7g.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-DWrpxFk0.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-MCpnpiKt.js">
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-CfVmBgkf.css">
12
12
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.11.13",
3
+ "version": "0.11.15",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,11 +28,11 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/ncp": "0.4.1",
32
- "@nextclaw/agent-chat-ui": "0.2.14",
33
- "@nextclaw/ncp-react": "0.4.5",
34
31
  "@nextclaw/agent-chat": "0.1.4",
35
- "@nextclaw/ncp-http-agent-client": "0.3.5"
32
+ "@nextclaw/ncp-http-agent-client": "0.3.6",
33
+ "@nextclaw/ncp": "0.4.2",
34
+ "@nextclaw/agent-chat-ui": "0.2.14",
35
+ "@nextclaw/ncp-react": "0.4.6"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/react": "^16.3.0",
@@ -175,4 +175,43 @@ describe('buildModelToolbarSelect', () => {
175
175
  }
176
176
  ]);
177
177
  });
178
+
179
+ it('preserves recent model order from newest to oldest', () => {
180
+ const select = buildModelToolbarSelect({
181
+ modelOptions: [
182
+ {
183
+ value: 'openai/gpt-5',
184
+ modelLabel: 'gpt-5',
185
+ providerLabel: 'OpenAI'
186
+ },
187
+ {
188
+ value: 'anthropic/claude-sonnet-4',
189
+ modelLabel: 'claude-sonnet-4',
190
+ providerLabel: 'Anthropic'
191
+ },
192
+ {
193
+ value: 'deepseek/deepseek-chat',
194
+ modelLabel: 'deepseek-chat',
195
+ providerLabel: 'DeepSeek'
196
+ }
197
+ ],
198
+ recentModelValues: ['deepseek/deepseek-chat', 'openai/gpt-5', 'anthropic/claude-sonnet-4'],
199
+ selectedModel: 'openai/gpt-5',
200
+ isModelOptionsLoading: false,
201
+ hasModelOptions: true,
202
+ onValueChange: vi.fn(),
203
+ texts: {
204
+ modelSelectPlaceholder: 'Select model',
205
+ modelNoOptionsLabel: 'No models',
206
+ recentModelsLabel: 'Recent',
207
+ allModelsLabel: 'All models'
208
+ }
209
+ });
210
+
211
+ expect(select.groups?.[0]?.options.map((option) => option.value)).toEqual([
212
+ 'deepseek/deepseek-chat',
213
+ 'openai/gpt-5',
214
+ 'anthropic/claude-sonnet-4'
215
+ ]);
216
+ });
178
217
  });
@@ -251,7 +251,10 @@ export function buildModelToolbarSelect(params: {
251
251
  const resolvedModelOption = selectedModelOption ?? fallbackModelOption;
252
252
  const resolvedValue = params.hasModelOptions ? resolvedModelOption?.value : undefined;
253
253
  const recentValueSet = new Set(params.recentModelValues ?? []);
254
- const recentOptions = params.modelOptions.filter((option) => recentValueSet.has(option.value));
254
+ const modelOptionMap = new Map(params.modelOptions.map((option) => [option.value, option] as const));
255
+ const recentOptions = (params.recentModelValues ?? [])
256
+ .map((value) => modelOptionMap.get(value))
257
+ .filter((option): option is ChatModelRecord => Boolean(option));
255
258
  const remainingOptions = params.modelOptions.filter((option) => !recentValueSet.has(option.value));
256
259
  const optionGroups =
257
260
  recentOptions.length > 0
@@ -141,6 +141,46 @@ it("maps tool lifecycle statuses into visible card state feedback", () => {
141
141
  });
142
142
  });
143
143
 
144
+ it("renders spawn tool cards from structured subagent status updates", () => {
145
+ const adapted = adapt([
146
+ {
147
+ id: "assistant-subagent",
148
+ role: "assistant",
149
+ parts: [
150
+ {
151
+ type: "tool-invocation",
152
+ toolInvocation: {
153
+ status: ToolInvocationStatus.RESULT,
154
+ toolCallId: "spawn-call-1",
155
+ toolName: "spawn",
156
+ args: '{"label":"Verifier","task":"Verify 1+1=2"}',
157
+ result: {
158
+ kind: "nextclaw.subagent_run",
159
+ runId: "subagent-1",
160
+ label: "Verifier",
161
+ task: "Verify 1+1=2",
162
+ status: "completed",
163
+ result: "Verified 1+1=2.",
164
+ },
165
+ },
166
+ },
167
+ ],
168
+ },
169
+ ] as unknown as ChatMessageSource[]);
170
+
171
+ expect(adapted[0]?.parts[0]).toMatchObject({
172
+ type: "tool-card",
173
+ card: {
174
+ toolName: "spawn",
175
+ summary: "label: Verifier · task: Verify 1+1=2",
176
+ output: "Verified 1+1=2.",
177
+ statusTone: "success",
178
+ statusLabel: "Completed",
179
+ titleLabel: "Tool Result",
180
+ },
181
+ });
182
+ });
183
+
144
184
  it("maps non-standard roles back to the generic message role", () => {
145
185
  const adapted = adapt([
146
186
  {
@@ -3,6 +3,7 @@ import {
3
3
  summarizeToolArgs,
4
4
  type ToolCard,
5
5
  } from "@/lib/chat-message";
6
+ import { buildSubagentToolCard } from "@/components/chat/adapters/chat-message.subagent-tool-card";
6
7
  import type {
7
8
  ChatMessageRole,
8
9
  ChatMessageViewModel,
@@ -386,6 +387,16 @@ export function adaptChatMessage(
386
387
  if (assetFileView) {
387
388
  return assetFileView;
388
389
  }
390
+ const subagentToolCard = buildSubagentToolCard({
391
+ invocation,
392
+ texts: params.texts,
393
+ });
394
+ if (subagentToolCard) {
395
+ return {
396
+ type: "tool-card" as const,
397
+ card: buildToolCard(subagentToolCard, params.texts),
398
+ };
399
+ }
389
400
  const statusView = resolveToolCardStatus({
390
401
  status: invocation.status,
391
402
  error: invocation.error,
@@ -0,0 +1,125 @@
1
+ import {
2
+ stringifyUnknown,
3
+ summarizeToolArgs,
4
+ type ToolCard,
5
+ } from "@/lib/chat-message";
6
+ import type { ChatToolPartViewModel } from "@nextclaw/agent-chat-ui";
7
+
8
+ type ToolCardViewSource = ToolCard & {
9
+ statusTone: ChatToolPartViewModel["statusTone"];
10
+ statusLabel: string;
11
+ };
12
+
13
+ type SpawnToolInvocation = {
14
+ toolName: string;
15
+ toolCallId?: string;
16
+ args?: unknown;
17
+ result?: unknown;
18
+ };
19
+
20
+ type SubagentToolCardTexts = {
21
+ toolStatusRunningLabel: string;
22
+ toolStatusCompletedLabel: string;
23
+ toolStatusFailedLabel: string;
24
+ };
25
+
26
+ type SubagentRunResult = {
27
+ runId?: string;
28
+ label?: string;
29
+ task?: string;
30
+ status?: string;
31
+ result?: unknown;
32
+ message?: string;
33
+ };
34
+
35
+ function isRecord(value: unknown): value is Record<string, unknown> {
36
+ return typeof value === "object" && value !== null;
37
+ }
38
+
39
+ function readOptionalString(value: unknown): string | null {
40
+ if (typeof value !== "string") {
41
+ return null;
42
+ }
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 ? trimmed : null;
45
+ }
46
+
47
+ function readSubagentRunResult(value: unknown): SubagentRunResult | null {
48
+ if (!isRecord(value)) {
49
+ return null;
50
+ }
51
+ if (value.kind === "nextclaw.subagent_run") {
52
+ return value;
53
+ }
54
+ if (typeof value.runId === "string" && typeof value.status === "string") {
55
+ return value;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ export function buildSubagentToolCard(params: {
61
+ invocation: SpawnToolInvocation;
62
+ texts: SubagentToolCardTexts;
63
+ }): ToolCardViewSource | null {
64
+ if (params.invocation.toolName !== "spawn") {
65
+ return null;
66
+ }
67
+
68
+ const subagentRun = readSubagentRunResult(params.invocation.result);
69
+ if (!subagentRun) {
70
+ return null;
71
+ }
72
+
73
+ const detailParts = [
74
+ readOptionalString(subagentRun.label)
75
+ ? `label: ${subagentRun.label?.trim()}`
76
+ : null,
77
+ readOptionalString(subagentRun.task)
78
+ ? `task: ${subagentRun.task?.trim()}`
79
+ : null,
80
+ ].filter((value): value is string => Boolean(value));
81
+ const normalizedStatus = readOptionalString(subagentRun.status)?.toLowerCase();
82
+ const output =
83
+ (typeof subagentRun.result !== "undefined"
84
+ ? stringifyUnknown(subagentRun.result).trim()
85
+ : "") ||
86
+ readOptionalString(subagentRun.message) ||
87
+ undefined;
88
+
89
+ if (normalizedStatus === "failed") {
90
+ return {
91
+ kind: "result",
92
+ name: params.invocation.toolName,
93
+ detail: detailParts.join(" · ") || summarizeToolArgs(params.invocation.args),
94
+ text: output,
95
+ callId: params.invocation.toolCallId || undefined,
96
+ hasResult: Boolean(output),
97
+ statusTone: "error",
98
+ statusLabel: params.texts.toolStatusFailedLabel,
99
+ };
100
+ }
101
+
102
+ if (normalizedStatus === "completed") {
103
+ return {
104
+ kind: "result",
105
+ name: params.invocation.toolName,
106
+ detail: detailParts.join(" · ") || summarizeToolArgs(params.invocation.args),
107
+ text: output,
108
+ callId: params.invocation.toolCallId || undefined,
109
+ hasResult: Boolean(output),
110
+ statusTone: "success",
111
+ statusLabel: params.texts.toolStatusCompletedLabel,
112
+ };
113
+ }
114
+
115
+ return {
116
+ kind: "result",
117
+ name: params.invocation.toolName,
118
+ detail: detailParts.join(" · ") || summarizeToolArgs(params.invocation.args),
119
+ text: output,
120
+ callId: params.invocation.toolCallId || undefined,
121
+ hasResult: Boolean(output),
122
+ statusTone: "running",
123
+ statusLabel: params.texts.toolStatusRunningLabel,
124
+ };
125
+ }
@@ -108,14 +108,10 @@ export function ChatInputBarContainer() {
108
108
  [snapshot.skillRecords, officialSkillBadgeLabel]
109
109
  );
110
110
  const modelRecords = useMemo(() => toModelRecords(snapshot.modelOptions), [snapshot.modelOptions]);
111
- const recentModelValues = useMemo(
112
- () =>
113
- chatRecentModelsManager.resolveVisible({
114
- availableValues: modelRecords.map((option) => option.value),
115
- minAvailableCount: CHAT_RECENT_MODELS_MIN_OPTIONS
116
- }),
117
- [modelRecords, snapshot.selectedModel]
118
- );
111
+ const recentModelValues = chatRecentModelsManager.resolveVisible({
112
+ availableValues: modelRecords.map((option) => option.value),
113
+ minAvailableCount: CHAT_RECENT_MODELS_MIN_OPTIONS
114
+ });
119
115
 
120
116
  const hasModelOptions = modelRecords.length > 0;
121
117
  const isModelOptionsLoading = !snapshot.isProviderStateResolved && !hasModelOptions;
@@ -128,6 +124,8 @@ export function ChatInputBarContainer() {
128
124
  : hasModelOptions
129
125
  ? t('chatInputPlaceholder')
130
126
  : t('chatModelNoOptions');
127
+ const recentModelsLabel = language === 'zh' ? '最近选择' : 'Recent';
128
+ const allModelsLabel = language === 'zh' ? '全部模型' : 'All models';
131
129
 
132
130
  const slashItems = useMemo(
133
131
  () => buildChatSlashItems(skillRecords, slashQuery ?? '', slashTexts),
@@ -191,8 +189,8 @@ export function ChatInputBarContainer() {
191
189
  texts: {
192
190
  modelSelectPlaceholder: t('chatSelectModel'),
193
191
  modelNoOptionsLabel: t('chatModelNoOptions'),
194
- recentModelsLabel: t('chatRecentModels'),
195
- allModelsLabel: t('chatAllModels')
192
+ recentModelsLabel,
193
+ allModelsLabel
196
194
  }
197
195
  }),
198
196
  buildThinkingToolbarSelect({
@@ -70,7 +70,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
70
70
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
71
71
  const threadRef = useRef<HTMLDivElement | null>(null);
72
72
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
73
- const pendingRealtimeReloadRef = useRef(false);
73
+ const sessionStreamAttachInFlightRef = useRef(false);
74
74
  const routeSessionKey = useMemo(
75
75
  () => parseSessionKeyFromRoute(routeSessionIdParam),
76
76
  [routeSessionIdParam]
@@ -157,39 +157,33 @@ export function NcpChatPage({ view }: ChatPageProps) {
157
157
  const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
158
158
 
159
159
  useEffect(() => {
160
- const flushRealtimeReload = () => {
160
+ const attachRealtimeSessionStream = () => {
161
+ if (sessionStreamAttachInFlightRef.current) {
162
+ return;
163
+ }
161
164
  if (agent.isHydrating || agent.isRunning || agent.isSending) {
162
- pendingRealtimeReloadRef.current = true;
163
165
  return;
164
166
  }
165
- pendingRealtimeReloadRef.current = false;
166
- void agent.reloadSeed();
167
+
168
+ sessionStreamAttachInFlightRef.current = true;
169
+ void ncpClient
170
+ .stream({ sessionId: activeSessionId })
171
+ .catch(() => undefined)
172
+ .finally(() => {
173
+ sessionStreamAttachInFlightRef.current = false;
174
+ });
167
175
  };
168
176
 
169
177
  return appClient.subscribe((event) => {
170
- if (event.type === 'session.summary.upsert') {
171
- if (event.payload.summary.sessionId !== activeSessionId) {
172
- return;
173
- }
174
- flushRealtimeReload();
178
+ if (event.type === 'session.updated' && event.payload.sessionKey === activeSessionId) {
179
+ attachRealtimeSessionStream();
175
180
  return;
176
181
  }
177
- if (event.type === 'session.updated' && event.payload.sessionKey === activeSessionId) {
178
- flushRealtimeReload();
182
+ if (event.type === 'session.summary.upsert' && event.payload.summary.sessionId === activeSessionId) {
183
+ attachRealtimeSessionStream();
179
184
  }
180
185
  });
181
- }, [activeSessionId, agent.isHydrating, agent.isRunning, agent.isSending, agent.reloadSeed]);
182
-
183
- useEffect(() => {
184
- if (!pendingRealtimeReloadRef.current) {
185
- return;
186
- }
187
- if (agent.isHydrating || agent.isRunning || agent.isSending) {
188
- return;
189
- }
190
- pendingRealtimeReloadRef.current = false;
191
- void agent.reloadSeed();
192
- }, [agent.isHydrating, agent.isRunning, agent.isSending, agent.reloadSeed]);
186
+ }, [activeSessionId, agent.isHydrating, agent.isRunning, agent.isSending, ncpClient]);
193
187
 
194
188
  useEffect(() => {
195
189
  presenter.chatStreamActionsManager.bind({
@@ -1,2 +1,2 @@
1
1
  ## 目录预算豁免
2
- - 原因:配置中心目录按配置面板维度组织,每个面板都需要独立入口、局部测试与装配文件;当前结构受 UI 信息架构约束,需要保留超过 `20` 个直接代码文件的扁平集合。
2
+ - 原因:配置中心目录按配置面板维度组织,每个面板都需要独立入口、局部测试与装配文件;当前结构受 UI 信息架构约束,需要保留达到或超过 `12` 个直接代码文件的扁平集合。
package/src/lib/i18n.ts CHANGED
@@ -185,8 +185,6 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
185
185
  zh: 'Agent 默认模型标识,使用带 provider 前缀的格式。例如:openai/gpt-5.1、anthropic/claude-opus-4-1、deepseek/deepseek-chat、minimax/MiniMax-M2.5、openrouter/openai/gpt-5.3-codex。',
186
186
  en: 'Default model identifier used by the agent. Use provider-prefixed format. Examples: openai/gpt-5.1 · anthropic/claude-opus-4-1 · deepseek/deepseek-chat · minimax/MiniMax-M2.5 · openrouter/openai/gpt-5.3-codex.'
187
187
  },
188
- chatRecentModels: { zh: '最近选择', en: 'Recent' },
189
- chatAllModels: { zh: '全部模型', en: 'All models' },
190
188
  maxToolIterations: { zh: '最大工具迭代次数', en: 'Max Tool Iterations' },
191
189
  saveChanges: { zh: '保存变更', en: 'Save Changes' },
192
190