@nextclaw/ui 0.15.17 → 0.15.19

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 (120) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/assets/api-O5yEgzBt.js +13 -0
  3. package/dist/assets/app-presenter-provider-DXTYtH78.js +106 -0
  4. package/dist/assets/appearance-settings-page-BuGxSvlJ.js +1 -0
  5. package/dist/assets/{book-open-WsEnuW9P.js → book-open-HtUSPwoS.js} +1 -1
  6. package/dist/assets/card-GkBc-UMw.js +1 -0
  7. package/dist/assets/{channels-list-page-CAWuTHSM.js → channels-list-page-Bspr1Jp4.js} +2 -2
  8. package/dist/assets/chat-page-BKQ89f4s.js +1 -0
  9. package/dist/assets/config-split-page-CvLXSFSC.js +1 -0
  10. package/dist/assets/confirm-dialog-BVH7haN0.js +5 -0
  11. package/dist/assets/desktop-update-config-C9ZUI8UW.js +1 -0
  12. package/dist/assets/{dist-Ca-jiFrK.js → dist-CZx9Q6jc.js} +1 -1
  13. package/dist/assets/doc-browser-B4l0Xu7u.js +1 -0
  14. package/dist/assets/doc-browser-DsVF1DrS.js +1 -0
  15. package/dist/assets/doc-browser-context-Dy7EhLI8.js +1 -0
  16. package/dist/assets/{eye-B30J471A.js → eye-DU7p1-7X.js} +1 -1
  17. package/dist/assets/form-actions-aM9_jSJC.js +1 -0
  18. package/dist/assets/index-6pHX0DZH.js +2 -0
  19. package/dist/assets/index-DN6KM7WD.css +1 -0
  20. package/dist/assets/loader-circle-Dd6jw700.js +1 -0
  21. package/dist/assets/logo-badge-2-iV1pkr.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-C-d0RMfg.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-Dg03SZX1.js +9 -0
  24. package/dist/assets/model-config-page-DOBqLZoQ.js +1 -0
  25. package/dist/assets/{navigation-link-qPPUiLt0.js → navigation-link-Ct6xtnVM.js} +1 -1
  26. package/dist/assets/plus-CsgeRId8.js +1 -0
  27. package/dist/assets/{popover-DHx55dbc.js → popover-C2oIpGBF.js} +1 -1
  28. package/dist/assets/provider-scoped-model-input-DDqCdnTU.js +1 -0
  29. package/dist/assets/{providers-config-page-BwHtHIPe.js → providers-config-page-BAVPJKpx.js} +1 -1
  30. package/dist/assets/react-CeMr9V52.js +8 -0
  31. package/dist/assets/{refresh-cw-C7bynrWX.js → refresh-cw-BZ8giusN.js} +1 -1
  32. package/dist/assets/remote-C-r0SHcz.js +1 -0
  33. package/dist/assets/rotate-cw-DVdRfMSO.js +1 -0
  34. package/dist/assets/{runtime-config-page-CbSrKeJt.js → runtime-config-page-D0244uxJ.js} +1 -1
  35. package/dist/assets/{save-CsRsFtHS.js → save-CSzFGRse.js} +1 -1
  36. package/dist/assets/{search-D0HtJIL-.js → search-BmSWsiG2.js} +1 -1
  37. package/dist/assets/{search-config-page-n1GElgij.js → search-config-page-CrL1pUzy.js} +1 -1
  38. package/dist/assets/{secrets-config-page-DbUc3Ggi.js → secrets-config-page-BlgsD0-L.js} +2 -2
  39. package/dist/assets/security-config-cDrdpMJW.js +1 -0
  40. package/dist/assets/{select-C1zfp9zM.js → select-CklWb6Rs.js} +1 -1
  41. package/dist/assets/{settings-2-Ba3cLSN2.js → settings-2-BSZf8327.js} +1 -1
  42. package/dist/assets/skeleton-DG9u1BmA.js +1 -0
  43. package/dist/assets/{tabs-DmKgMKzb.js → tabs-Cah5klko.js} +1 -1
  44. package/dist/assets/{tag-chip-2vwlVUwM.js → tag-chip-DcrirDgN.js} +1 -1
  45. package/dist/assets/{trash-2-CUpWjNHu.js → trash-2-BLQOULWD.js} +1 -1
  46. package/dist/assets/use-config-CuFIsUVG.js +1 -0
  47. package/dist/assets/use-confirm-dialog-CcySmkWp.js +1 -0
  48. package/dist/assets/{use-viewport-layout-DaKJ-kap.js → use-viewport-layout-DU29bUJS.js} +1 -1
  49. package/dist/assets/x-BVoeXny1.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +11 -0
  53. package/src/features/chat/components/conversation/chat-conversation-content.tsx +7 -1
  54. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +50 -4
  55. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +25 -1
  56. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +3 -0
  57. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +41 -0
  58. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-slash-commands.test.tsx +28 -0
  59. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-query.ts +1 -1
  60. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +0 -1
  61. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +10 -11
  62. package/src/features/chat/features/conversation/hooks/use-session-conversation-slash-commands.ts +5 -2
  63. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +35 -0
  64. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +3 -1
  65. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +13 -5
  66. package/src/features/chat/features/message/components/chat-message-list.container.tsx +13 -9
  67. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +9 -0
  68. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +4 -1
  69. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +29 -0
  70. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  71. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +122 -1
  72. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +31 -6
  73. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +107 -1
  74. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +41 -0
  75. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +4 -0
  76. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +15 -2
  77. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +11 -0
  78. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +1 -0
  79. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +11 -8
  80. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +6 -0
  81. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  82. package/src/features/chat/managers/chat-ui.manager.ts +10 -4
  83. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  84. package/src/features/chat/stores/chat-thread.store.ts +6 -1
  85. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +32 -0
  86. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +2 -0
  87. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +20 -38
  88. package/src/shared/components/doc-browser/types/doc-browser.types.ts +5 -0
  89. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +63 -0
  90. package/src/shared/lib/i18n/locales/en-US/chat.json +1 -0
  91. package/src/shared/lib/i18n/locales/zh-CN/chat.json +1 -0
  92. package/dist/assets/api-D5UCLVE_.js +0 -13
  93. package/dist/assets/app-presenter-provider-C9AcZKKs.js +0 -106
  94. package/dist/assets/appearance-settings-page-CiD9Ey8f.js +0 -1
  95. package/dist/assets/card-xVrS1b1T.js +0 -1
  96. package/dist/assets/chat-page-B0cC3jsB.js +0 -1
  97. package/dist/assets/config-split-page-xj0UtMow.js +0 -1
  98. package/dist/assets/confirm-dialog-e2YCUzt1.js +0 -5
  99. package/dist/assets/desktop-update-config-D5NIaimo.js +0 -1
  100. package/dist/assets/doc-browser-B2tNEKrJ.js +0 -1
  101. package/dist/assets/doc-browser-CZX6I6CU.js +0 -1
  102. package/dist/assets/doc-browser-context-CGz0LULR.js +0 -1
  103. package/dist/assets/form-actions-CnSzu3WD.js +0 -1
  104. package/dist/assets/index-QveCjxCj.css +0 -1
  105. package/dist/assets/index-bOqpao-w.js +0 -2
  106. package/dist/assets/loader-circle-DjwSRQHW.js +0 -1
  107. package/dist/assets/logo-badge-CISsltAo.js +0 -1
  108. package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +0 -1
  109. package/dist/assets/mcp-marketplace-page-CPcPzMUQ.js +0 -9
  110. package/dist/assets/model-config-page-D6ex3_NM.js +0 -1
  111. package/dist/assets/plus-C45awBb1.js +0 -1
  112. package/dist/assets/provider-scoped-model-input-1TJxAhx6.js +0 -1
  113. package/dist/assets/react-DerwD2EA.js +0 -8
  114. package/dist/assets/remote-B0nlynVp.js +0 -1
  115. package/dist/assets/rotate-cw-Bhx0XkQP.js +0 -1
  116. package/dist/assets/security-config-n2WHhHXV.js +0 -1
  117. package/dist/assets/skeleton-DL5npsVL.js +0 -1
  118. package/dist/assets/use-config-BvxDWV-M.js +0 -1
  119. package/dist/assets/use-confirm-dialog-CeUEE4p5.js +0 -1
  120. package/dist/assets/x-DGdYT7rH.js +0 -1
@@ -7,7 +7,10 @@ import {
7
7
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
8
8
  import type { NcpMessage } from "@nextclaw/ncp";
9
9
  import { ArrowDown } from "lucide-react";
10
- import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
10
+ import {
11
+ ChatContextCompactionDivider,
12
+ ChatMessageListContainer,
13
+ } from "@/features/chat/features/message/components/chat-message-list.container";
11
14
  import { ChatConversationTrack } from "@/features/chat/components/conversation/chat-conversation-track";
12
15
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
13
16
  import { t } from "@/shared/lib/i18n";
@@ -19,6 +22,7 @@ type ChatConversationContentProps = {
19
22
  hasPreviousMessages: boolean;
20
23
  historyError: Error | null;
21
24
  isLoadingPreviousMessages: boolean;
25
+ isContextCompacting?: boolean;
22
26
  isSending: boolean;
23
27
  messages: readonly NcpMessage[];
24
28
  sessionKey: string | null;
@@ -34,6 +38,7 @@ export function ChatConversationContent({
34
38
  hasPreviousMessages,
35
39
  historyError,
36
40
  isLoadingPreviousMessages,
41
+ isContextCompacting = false,
37
42
  isSending,
38
43
  messages,
39
44
  sessionKey,
@@ -110,6 +115,7 @@ export function ChatConversationContent({
110
115
  scrollRef={threadRef}
111
116
  sessionKey={sessionKey}
112
117
  />
118
+ {isContextCompacting ? <ChatContextCompactionDivider /> : null}
113
119
  </ChatConversationTrack>
114
120
  ) : null}
115
121
  {bottomSlot ? (
@@ -1,5 +1,5 @@
1
1
  import { memo, type ReactNode } from "react";
2
- import { render, screen } from "@testing-library/react";
2
+ import { act, render, screen } from "@testing-library/react";
3
3
  import { MemoryRouter } from "react-router-dom";
4
4
  import { describe, expect, it, vi, beforeEach } from "vitest";
5
5
 
@@ -20,7 +20,6 @@ const mocks = vi.hoisted(() => {
20
20
  setAttachments: vi.fn(),
21
21
  addAttachments: vi.fn(() => []),
22
22
  removeAttachment: vi.fn(),
23
- setSelectedModel: vi.fn(),
24
23
  setSelectedThinkingLevel: vi.fn(),
25
24
  syncSessionPreferences: vi.fn(),
26
25
  setPendingSessionType: vi.fn(),
@@ -43,9 +42,9 @@ const mocks = vi.hoisted(() => {
43
42
  sendError: null,
44
43
  };
45
44
  const inputQuery = {
46
- defaultModel: undefined,
45
+ defaultModel: undefined as string | undefined,
47
46
  defaultProjectRoot: null,
48
- fallbackPreferredModel: undefined,
47
+ fallbackPreferredModel: undefined as string | undefined,
49
48
  fallbackPreferredThinking: null,
50
49
  isProviderStateResolved: true,
51
50
  isSkillsLoading: false,
@@ -138,17 +137,20 @@ vi.mock(
138
137
  () => ({
139
138
  ChatConversationContent: ({
140
139
  bottomSlot,
140
+ isContextCompacting,
141
141
  messages,
142
142
  showWelcome,
143
143
  welcomeSlot,
144
144
  }: {
145
145
  bottomSlot?: ReactNode;
146
+ isContextCompacting?: boolean;
146
147
  messages: readonly unknown[];
147
148
  showWelcome: boolean;
148
149
  welcomeSlot?: ReactNode;
149
150
  }) => (
150
151
  <div
151
152
  data-testid="conversation-content"
153
+ data-context-compacting={String(Boolean(isContextCompacting))}
152
154
  data-show-welcome={String(showWelcome)}
153
155
  >
154
156
  {showWelcome ? (
@@ -264,7 +266,10 @@ describe("SessionConversationArea input boundary", () => {
264
266
  mocks.agent.isRunning = true;
265
267
  mocks.agent.isSending = true;
266
268
  mocks.agent.snapshot.contextWindow = null;
269
+ mocks.inputQuery.defaultModel = undefined;
270
+ mocks.inputQuery.fallbackPreferredModel = undefined;
267
271
  mocks.inputQuery.selectedSession = null;
272
+ mocks.inputQuery.sessionTypeState.selectedSessionType = "default";
268
273
  });
269
274
 
270
275
  it("passes running state without treating an active run as a send request in flight", () => {
@@ -297,6 +302,23 @@ describe("SessionConversationArea input boundary", () => {
297
302
  expect(mocks.inputRenderSpy).toHaveBeenCalledOnce();
298
303
  });
299
304
 
305
+ it("shows compaction feedback only for the active session", () => {
306
+ renderArea("session-1");
307
+ const inputProps = mocks.inputRenderSpy.mock.calls[0]?.[0] as {
308
+ onContextCompactingChange: (sessionId: string, isCompacting: boolean) => void;
309
+ };
310
+
311
+ act(() => inputProps.onContextCompactingChange("session-1", true));
312
+
313
+ expect(screen.getByTestId("conversation-content").dataset.contextCompacting)
314
+ .toBe("true");
315
+
316
+ act(() => inputProps.onContextCompactingChange("session-1", false));
317
+
318
+ expect(screen.getByTestId("conversation-content").dataset.contextCompacting)
319
+ .toBe("false");
320
+ });
321
+
300
322
  it("does not replace the welcome composer just because draft send starts", () => {
301
323
  renderArea(null);
302
324
 
@@ -330,6 +352,30 @@ describe("SessionConversationArea input boundary", () => {
330
352
  expect(mocks.initialPromptSpy).toHaveBeenCalledWith("每天整理项目风险");
331
353
  });
332
354
 
355
+ it("syncs draft preferences with the selected runtime context", () => {
356
+ mocks.inputQuery.defaultModel = "openai/gpt-5";
357
+ mocks.inputQuery.fallbackPreferredModel = "minimax/MiniMax-M3";
358
+ const rendered = renderArea(null);
359
+ mocks.inputActions.syncSessionPreferences.mockClear();
360
+
361
+ mocks.inputQuery.sessionTypeState.selectedSessionType = "codex";
362
+ rendered.rerender(
363
+ <MemoryRouter>
364
+ <SessionConversationArea sessionKey={null} />
365
+ </MemoryRouter>,
366
+ );
367
+
368
+ expect(mocks.inputActions.syncSessionPreferences).toHaveBeenCalledOnce();
369
+ expect(mocks.inputActions.syncSessionPreferences).toHaveBeenLastCalledWith(
370
+ expect.objectContaining({
371
+ defaultModel: "openai/gpt-5",
372
+ fallbackPreferredModel: "minimax/MiniMax-M3",
373
+ selectedSessionKey: null,
374
+ selectedSessionType: "codex",
375
+ }),
376
+ );
377
+ });
378
+
333
379
  it("surfaces selected-session failure previews at the conversation bottom", () => {
334
380
  mocks.inputQuery.selectedSession = {
335
381
  activityPreview: {
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useRef } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useLocation } from "react-router-dom";
3
3
 
4
4
  import { useAppPresenter } from "@/app/components/app-presenter-provider";
@@ -200,6 +200,23 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
200
200
  inputSnapshot,
201
201
  setPendingSessionType: inputActions.setPendingSessionType,
202
202
  });
203
+ const [compactingSessionIds, setCompactingSessionIds] = useState<ReadonlySet<string>>(
204
+ () => new Set(),
205
+ );
206
+ const handleContextCompactingChange = useCallback(
207
+ (compactingSessionId: string, isCompacting: boolean) => {
208
+ setCompactingSessionIds((current) => {
209
+ const next = new Set(current);
210
+ if (isCompacting) {
211
+ next.add(compactingSessionId);
212
+ } else {
213
+ next.delete(compactingSessionId);
214
+ }
215
+ return next;
216
+ });
217
+ },
218
+ [],
219
+ );
203
220
  useEffect(() => {
204
221
  inputActions.syncSessionPreferences({
205
222
  defaultModel: inputQuery.defaultModel,
@@ -208,6 +225,7 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
208
225
  modelOptions: inputQuery.modelOptions,
209
226
  selectedSessionExists: Boolean(inputQuery.selectedSession),
210
227
  selectedSessionKey: inputQuery.selectedSessionKey,
228
+ selectedSessionType: inputQuery.sessionTypeState.selectedSessionType,
211
229
  selectedSessionPreferredModel:
212
230
  inputQuery.selectedSession?.preferredModel ?? undefined,
213
231
  selectedSessionPreferredThinking:
@@ -221,6 +239,7 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
221
239
  inputQuery.modelOptions,
222
240
  inputQuery.selectedSession,
223
241
  inputQuery.selectedSessionKey,
242
+ inputQuery.sessionTypeState.selectedSessionType,
224
243
  ]);
225
244
  useSessionConversationDraftRouteState({
226
245
  sessionKey,
@@ -336,6 +355,7 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
336
355
  inputActions={inputActions}
337
356
  inputQuery={inputQuery}
338
357
  inputSnapshot={displayInputSnapshot}
358
+ onContextCompactingChange={handleContextCompactingChange}
339
359
  surface={surface}
340
360
  />
341
361
  ),
@@ -345,6 +365,7 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
345
365
  inputController,
346
366
  inputActions,
347
367
  inputQuery,
368
+ handleContextCompactingChange,
348
369
  ],
349
370
  );
350
371
  const showWelcome =
@@ -365,6 +386,9 @@ export function SessionConversationArea(props: SessionConversationAreaProps) {
365
386
  isHistoryLoading={agent.isHydrating}
366
387
  isLoadingPreviousMessages={agent.isLoadingPreviousMessages}
367
388
  isSending={controller.isSending}
389
+ isContextCompacting={Boolean(
390
+ sessionKey && compactingSessionIds.has(sessionKey),
391
+ )}
368
392
  bottomSlot={sessionFailureSlot}
369
393
  messages={agent.visibleMessages}
370
394
  sessionKey={sessionKey}
@@ -192,6 +192,7 @@ type SessionConversationInputProps = {
192
192
  readonly inputActions: SessionConversationInputActions;
193
193
  readonly inputQuery: SessionConversationInputQuery;
194
194
  readonly inputSnapshot: SessionConversationInputSnapshot;
195
+ readonly onContextCompactingChange?: (sessionId: string, isCompacting: boolean) => void;
195
196
  readonly surface?: 'default' | 'embedded';
196
197
  };
197
198
 
@@ -202,6 +203,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
202
203
  inputActions,
203
204
  inputQuery,
204
205
  inputSnapshot,
206
+ onContextCompactingChange,
205
207
  surface = 'default',
206
208
  } = props;
207
209
  const presenter = usePresenter();
@@ -225,6 +227,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
225
227
  });
226
228
  const slashCommands = useSessionConversationSlashCommands({
227
229
  language,
230
+ onContextCompactingChange,
228
231
  selectedSessionKey: inputQuery.selectedSessionKey,
229
232
  });
230
233
  const handleSlashPanelAppSelect = useCallback(
@@ -36,6 +36,7 @@ describe('useSessionConversationInputState session preferences', () => {
36
36
  modelOptions: MODEL_OPTIONS,
37
37
  selectedSessionExists: true,
38
38
  selectedSessionKey: 'session-a',
39
+ selectedSessionType: 'native',
39
40
  selectedSessionPreferredModel: 'deepseek/deepseek-v4-flash',
40
41
  });
41
42
  });
@@ -46,6 +47,7 @@ describe('useSessionConversationInputState session preferences', () => {
46
47
  modelOptions: MODEL_OPTIONS,
47
48
  selectedSessionExists: false,
48
49
  selectedSessionKey: 'session-b',
50
+ selectedSessionType: 'native',
49
51
  });
50
52
  });
51
53
  expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
@@ -55,9 +57,48 @@ describe('useSessionConversationInputState session preferences', () => {
55
57
  modelOptions: MODEL_OPTIONS,
56
58
  selectedSessionExists: true,
57
59
  selectedSessionKey: 'session-b',
60
+ selectedSessionType: 'native',
58
61
  selectedSessionPreferredModel: 'minimax/MiniMax-M3',
59
62
  });
60
63
  });
61
64
  expect(result.current.inputSnapshot.selectedModel).toBe('minimax/MiniMax-M3');
62
65
  });
66
+
67
+ it('prefers the runtime recent model and otherwise restores the runtime default in a draft', () => {
68
+ const { result } = renderHook(() => useSessionConversationInputState());
69
+
70
+ act(() => {
71
+ result.current.inputActions.syncSessionPreferences({
72
+ fallbackPreferredModel: 'deepseek/deepseek-v4-flash',
73
+ modelOptions: MODEL_OPTIONS,
74
+ selectedSessionExists: false,
75
+ selectedSessionKey: null,
76
+ selectedSessionType: 'native',
77
+ });
78
+ });
79
+ expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
80
+
81
+ act(() => {
82
+ result.current.inputActions.syncSessionPreferences({
83
+ defaultModel: 'deepseek/deepseek-v4-flash',
84
+ fallbackPreferredModel: 'minimax/MiniMax-M3',
85
+ modelOptions: MODEL_OPTIONS,
86
+ selectedSessionExists: false,
87
+ selectedSessionKey: null,
88
+ selectedSessionType: 'codex',
89
+ });
90
+ });
91
+ expect(result.current.inputSnapshot.selectedModel).toBe('minimax/MiniMax-M3');
92
+
93
+ act(() => {
94
+ result.current.inputActions.syncSessionPreferences({
95
+ defaultModel: 'deepseek/deepseek-v4-flash',
96
+ modelOptions: MODEL_OPTIONS,
97
+ selectedSessionExists: false,
98
+ selectedSessionKey: null,
99
+ selectedSessionType: 'hermes',
100
+ });
101
+ });
102
+ expect(result.current.inputSnapshot.selectedModel).toBe('deepseek/deepseek-v4-flash');
103
+ });
63
104
  });
@@ -68,6 +68,34 @@ describe('useSessionConversationSlashCommands', () => {
68
68
  ));
69
69
  });
70
70
 
71
+ it('reports visible compaction state for the lifetime of the request', async () => {
72
+ let resolveRequest!: () => void;
73
+ mocks.compactNcpSessionContext.mockReturnValue(new Promise((resolve) => {
74
+ resolveRequest = () => resolve({
75
+ compacted: true,
76
+ sessionId: 'session-1',
77
+ });
78
+ }));
79
+ const onContextCompactingChange = vi.fn();
80
+ const { result } = renderHook(() => useSessionConversationSlashCommands({
81
+ language: 'en',
82
+ selectedSessionKey: 'session-1',
83
+ onContextCompactingChange,
84
+ }));
85
+
86
+ act(() => result.current.find((entry) => entry.key === 'compact-context')?.onSelect());
87
+
88
+ expect(onContextCompactingChange).toHaveBeenCalledWith('session-1', true);
89
+ expect(onContextCompactingChange).not.toHaveBeenCalledWith('session-1', false);
90
+
91
+ act(() => resolveRequest());
92
+
93
+ await waitFor(() => expect(onContextCompactingChange).toHaveBeenLastCalledWith(
94
+ 'session-1',
95
+ false,
96
+ ));
97
+ });
98
+
71
99
  it('does not expose session commands before a session exists', () => {
72
100
  const { result } = renderHook(() => useSessionConversationSlashCommands({
73
101
  language: 'en',
@@ -124,7 +124,7 @@ export function useSessionConversationInputQuery(params: UseSessionConversationI
124
124
  );
125
125
 
126
126
  return useMemo(() => ({
127
- defaultModel: config?.agents.defaults.model,
127
+ defaultModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? config?.agents.defaults.model,
128
128
  defaultProjectRoot,
129
129
  fallbackPreferredModel,
130
130
  fallbackPreferredThinking,
@@ -53,7 +53,6 @@ export type SessionConversationInputActions = {
53
53
  readonly setAttachments: (attachments: readonly NcpDraftAttachment[]) => void;
54
54
  readonly addAttachments: (attachments: readonly NcpDraftAttachment[]) => readonly NcpDraftAttachment[];
55
55
  readonly removeAttachment: (attachmentId: string) => void;
56
- readonly setSelectedModel: (model: SessionConversationInputStateValue) => void;
57
56
  readonly setSelectedThinkingLevel: (level: ThinkingLevel | null) => void;
58
57
  readonly syncSessionPreferences: (params: SessionConversationPreferenceSyncParams) => void;
59
58
  readonly setPendingSessionType: (sessionType: SetStateAction<string>) => void;
@@ -16,6 +16,7 @@ export type SessionConversationPreferenceSyncParams = {
16
16
  readonly modelOptions: ChatModelOption[];
17
17
  readonly selectedSessionExists: boolean;
18
18
  readonly selectedSessionKey?: string | null;
19
+ readonly selectedSessionType: string;
19
20
  readonly selectedSessionPreferredModel?: string;
20
21
  readonly selectedSessionPreferredThinking?: ThinkingLevel | null;
21
22
  };
@@ -34,10 +35,7 @@ export const useSessionConversationPreferenceActions = ({
34
35
  selectedThinkingLevel: currentSelectedThinkingLevel,
35
36
  updatePreferences,
36
37
  }: SessionConversationPreferenceActionsParams) => {
37
- const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
38
- const setSelectedModel = useCallback((selectedModel: ConversationPreferenceValue) => {
39
- updatePreferences({ selectedModel });
40
- }, [updatePreferences]);
38
+ const previousPreferenceContextRef = useRef<string | undefined>(undefined);
41
39
  const setSelectedThinkingLevel = useCallback((selectedThinkingLevel: ThinkingLevel | null) => {
42
40
  updatePreferences({ selectedThinkingLevel });
43
41
  }, [updatePreferences]);
@@ -48,19 +46,21 @@ export const useSessionConversationPreferenceActions = ({
48
46
  modelOptions,
49
47
  selectedSessionExists,
50
48
  selectedSessionKey: sessionKey,
49
+ selectedSessionType,
51
50
  selectedSessionPreferredModel,
52
51
  selectedSessionPreferredThinking,
53
52
  }: SessionConversationPreferenceSyncParams) => {
54
53
  const selectedSessionKey = sessionKey ?? null;
55
- const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
56
- const preserveCurrentPreference = sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists;
54
+ const preferenceContextKey = JSON.stringify([selectedSessionType, selectedSessionKey]);
55
+ const preferenceContextChanged = previousPreferenceContextRef.current !== preferenceContextKey;
56
+ const preserveCurrentPreference = preferenceContextChanged && Boolean(selectedSessionKey) && !selectedSessionExists;
57
57
  const selectedModel = resolveSelectedModelValue({
58
58
  currentSelectedModel: currentSelectedModel ?? undefined,
59
59
  modelOptions,
60
60
  selectedSessionPreferredModel,
61
61
  fallbackPreferredModel,
62
62
  defaultModel,
63
- preferSessionPreferredModel: sessionChanged,
63
+ preferSessionPreferredModel: preferenceContextChanged,
64
64
  preserveCurrentSelectedModelOnSessionChange: preserveCurrentPreference,
65
65
  });
66
66
  const modelOption = modelOptions.find((option) => option.value === selectedModel);
@@ -72,7 +72,7 @@ export const useSessionConversationPreferenceActions = ({
72
72
  fallbackPreferredThinking,
73
73
  defaultThinkingLevel:
74
74
  (modelOption?.thinkingCapability?.default as ThinkingLevel | null | undefined) ?? null,
75
- preferSessionPreferredThinking: sessionChanged,
75
+ preferSessionPreferredThinking: preferenceContextChanged,
76
76
  preserveCurrentSelectedThinkingOnSessionChange: preserveCurrentPreference,
77
77
  });
78
78
  if (
@@ -82,13 +82,12 @@ export const useSessionConversationPreferenceActions = ({
82
82
  updatePreferences({ selectedModel, selectedThinkingLevel });
83
83
  }
84
84
  if (!selectedSessionKey || selectedSessionExists) {
85
- previousSessionKeyRef.current = selectedSessionKey;
85
+ previousPreferenceContextRef.current = preferenceContextKey;
86
86
  }
87
87
  }, [currentSelectedModel, currentSelectedThinkingLevel, updatePreferences]);
88
88
 
89
89
  return useMemo(() => ({
90
- setSelectedModel,
91
90
  setSelectedThinkingLevel,
92
91
  syncSessionPreferences,
93
- }), [setSelectedModel, setSelectedThinkingLevel, syncSessionPreferences]);
92
+ }), [setSelectedThinkingLevel, syncSessionPreferences]);
94
93
  };
@@ -7,9 +7,10 @@ import type { ChatSlashCommandDescriptor } from '@/features/chat/features/input/
7
7
 
8
8
  export function useSessionConversationSlashCommands(params: {
9
9
  language: I18nLanguage;
10
+ onContextCompactingChange?: (sessionId: string, isCompacting: boolean) => void;
10
11
  selectedSessionKey?: string | null;
11
12
  }): readonly ChatSlashCommandDescriptor[] {
12
- const { language, selectedSessionKey } = params;
13
+ const { language, onContextCompactingChange, selectedSessionKey } = params;
13
14
  const presenter = usePresenter();
14
15
  const compactingSessionIdsRef = useRef(new Set<string>());
15
16
  const compactContext = useCallback(async (sessionId: string) => {
@@ -17,6 +18,7 @@ export function useSessionConversationSlashCommands(params: {
17
18
  return;
18
19
  }
19
20
  compactingSessionIdsRef.current.add(sessionId);
21
+ onContextCompactingChange?.(sessionId, true);
20
22
  try {
21
23
  await compactNcpSessionContext(sessionId);
22
24
  toast.success(t('chatSlashCommandCompactContextSuccess', language));
@@ -25,8 +27,9 @@ export function useSessionConversationSlashCommands(params: {
25
27
  toast.error(`${t('chatSlashCommandCompactContextFailed', language)}: ${message}`);
26
28
  } finally {
27
29
  compactingSessionIdsRef.current.delete(sessionId);
30
+ onContextCompactingChange?.(sessionId, false);
28
31
  }
29
- }, [language]);
32
+ }, [language, onContextCompactingChange]);
30
33
 
31
34
  return useMemo(() => {
32
35
  const sessionId = selectedSessionKey?.trim();
@@ -72,6 +72,9 @@ it("renders panel apps through the shared adaptive inline surface", () => {
72
72
  <ChatInlinePanelAppCard
73
73
  panelApp={{
74
74
  appId: "weather-card",
75
+ params: {
76
+ city: "Shanghai",
77
+ },
75
78
  title: "Weather",
76
79
  }}
77
80
  />,
@@ -99,6 +102,8 @@ it("renders panel apps through the shared adaptive inline surface", () => {
99
102
  expect(iframe.getAttribute("src")).toBe(
100
103
  "/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline",
101
104
  );
105
+ expect(iframe.name).toContain('"city":"Shanghai"');
106
+ expect(iframe.getAttribute("src")).not.toContain("Shanghai");
102
107
 
103
108
  fireEvent(
104
109
  window,
@@ -118,6 +123,9 @@ it("renders panel apps through the shared adaptive inline surface", () => {
118
123
 
119
124
  expect(mocks.openTarget).toHaveBeenCalledWith(
120
125
  expect.objectContaining({
126
+ contentParams: {
127
+ city: "Shanghai",
128
+ },
121
129
  dedupeKey: "panel-app:weather-card",
122
130
  kind: "panel-app",
123
131
  title: "Weather",
@@ -126,6 +134,33 @@ it("renders panel apps through the shared adaptive inline surface", () => {
126
134
  );
127
135
  });
128
136
 
137
+ it("replaces the inline iframe when params change at the same URL", () => {
138
+ const { rerender } = render(
139
+ <ChatInlinePanelAppCard
140
+ panelApp={{
141
+ appId: "weather-card",
142
+ params: { city: "Shanghai" },
143
+ title: "Weather",
144
+ }}
145
+ />,
146
+ );
147
+ const firstIframe = screen.getByTitle("Weather");
148
+
149
+ rerender(
150
+ <ChatInlinePanelAppCard
151
+ panelApp={{
152
+ appId: "weather-card",
153
+ params: { city: "Beijing" },
154
+ title: "Weather",
155
+ }}
156
+ />,
157
+ );
158
+
159
+ const nextIframe = screen.getByTitle("Weather") as HTMLIFrameElement;
160
+ expect(nextIframe).not.toBe(firstIframe);
161
+ expect(nextIframe.name).toContain('"city":"Beijing"');
162
+ });
163
+
129
164
  it("renders and expands a panel app from an explicit external path", () => {
130
165
  render(
131
166
  <ChatInlinePanelAppCard
@@ -31,7 +31,7 @@ export function ChatInlineFilePreview({
31
31
  if (display.target.type !== "file") {
32
32
  return null;
33
33
  }
34
- const { path, line, column, viewer } = display.target.payload;
34
+ const { path, line, column, params, viewer } = display.target.payload;
35
35
  const file = createWorkspaceFileTab(
36
36
  {
37
37
  path,
@@ -40,6 +40,7 @@ export function ChatInlineFilePreview({
40
40
  previewViewer: viewer,
41
41
  line,
42
42
  column,
43
+ params,
43
44
  },
44
45
  parentSessionKey,
45
46
  );
@@ -58,6 +59,7 @@ export function ChatInlineFilePreview({
58
59
  previewViewer,
59
60
  line: file.line ?? undefined,
60
61
  column: file.column ?? undefined,
62
+ params: file.params ?? undefined,
61
63
  });
62
64
  };
63
65
 
@@ -19,10 +19,11 @@ import { ChatInlineContentSurface } from "@/features/chat/features/message/compo
19
19
  import { useDocBrowser } from "@/shared/components/doc-browser";
20
20
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
21
21
  import { t } from "@/shared/lib/i18n";
22
+ import { createUiContentParamsWindowName } from "@nextclaw/shared";
22
23
 
23
24
  type ChatInlinePanelAppDescriptor = Pick<
24
25
  ChatPanelAppCardViewModel,
25
- "appId" | "path" | "title"
26
+ "appId" | "params" | "path" | "title"
26
27
  >;
27
28
 
28
29
  export function ChatInlinePanelAppCard({
@@ -60,21 +61,27 @@ export function ChatInlinePanelAppCard({
60
61
  () =>
61
62
  createInlinePanelAppTab({
62
63
  appId: entry?.id ?? panelApp.appId,
64
+ params: panelApp.params,
63
65
  path: panelApp.path,
64
66
  title,
65
67
  url,
66
- }),
67
- [entry?.id, panelApp.appId, panelApp.path, title, url],
68
+ }),
69
+ [entry?.id, panelApp.appId, panelApp.params, panelApp.path, title, url],
68
70
  );
69
- const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}`;
71
+ const contentParamsWindowName = createUiContentParamsWindowName(panelApp.params);
72
+ const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}:${contentParamsWindowName ?? ""}`;
70
73
  const openExpanded = () => {
71
74
  if (target) {
72
- docBrowser.openTarget(target);
75
+ docBrowser.openTarget({
76
+ ...target,
77
+ contentParams: panelApp.params,
78
+ });
73
79
  return;
74
80
  }
75
81
  docBrowser.open(url, {
76
82
  dedupeKey: tab.dedupeKey,
77
83
  kind: tab.kind,
84
+ contentParams: panelApp.params,
78
85
  title,
79
86
  });
80
87
  };
@@ -130,6 +137,7 @@ export function ChatInlinePanelAppCard({
130
137
  ref={iframeRef}
131
138
  key={iframeInstanceId}
132
139
  src={cardUrl}
140
+ name={contentParamsWindowName}
133
141
  title={title}
134
142
  sandbox={PANEL_APP_IFRAME_SANDBOX}
135
143
  tabIndex={0}
@@ -145,6 +145,7 @@ function renderChatInlineDisplay(display: ChatInlineDisplayViewModel) {
145
145
  <ChatInlinePanelAppCard
146
146
  panelApp={{
147
147
  appId: display.target.payload.appId,
148
+ params: display.target.payload.params,
148
149
  path: display.target.payload.path,
149
150
  title: display.title,
150
151
  }}
@@ -159,16 +160,19 @@ const renderChatPanelAppCard = (panelApp: ChatPanelAppCardViewModel) => (
159
160
  <ChatInlinePanelAppCard panelApp={panelApp} />
160
161
  );
161
162
 
162
- function ChatContextCompactionDivider({
163
+ export function ChatContextCompactionDivider({
163
164
  checkpoint,
164
165
  }: {
165
- checkpoint: ContextCompactionTimelineView;
166
+ checkpoint?: ContextCompactionTimelineView;
166
167
  }) {
167
- const title = [
168
- `${t("chatContextCompactionCoveredMessages")}: ${checkpoint.coveredSessionMessageCount}`,
169
- `${t("chatContextCompactionOriginalTokens")}: ${checkpoint.originalEstimatedTokens}`,
170
- `${t("chatContextCompactionProjectedTokens")}: ${checkpoint.projectedEstimatedTokens}`,
171
- ].join("\n");
168
+ const isCompacting = !checkpoint || checkpoint.status === "compressing";
169
+ const title = checkpoint
170
+ ? [
171
+ `${t("chatContextCompactionCoveredMessages")}: ${checkpoint.coveredSessionMessageCount}`,
172
+ `${t("chatContextCompactionOriginalTokens")}: ${checkpoint.originalEstimatedTokens}`,
173
+ `${t("chatContextCompactionProjectedTokens")}: ${checkpoint.projectedEstimatedTokens}`,
174
+ ].join("\n")
175
+ : undefined;
172
176
  return (
173
177
  <div
174
178
  className="my-4 flex items-center gap-3 text-[11px] text-muted-foreground"
@@ -176,13 +180,13 @@ function ChatContextCompactionDivider({
176
180
  >
177
181
  <div className="h-px flex-1 bg-border" />
178
182
  <div className="inline-flex items-center gap-2 rounded-full border border-border bg-muted px-3 py-1">
179
- {checkpoint.status === "compressing" ? (
183
+ {isCompacting ? (
180
184
  <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-muted-foreground" />
181
185
  ) : (
182
186
  <span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/45" />
183
187
  )}
184
188
  <span>
185
- {checkpoint.status === "compressing"
189
+ {isCompacting
186
190
  ? t("chatContextCompactionCompressing")
187
191
  : t("chatContextCompactionCompressed")}
188
192
  </span>