@nextclaw/ui 0.12.19 → 0.12.20-beta.0

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 (152) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/assets/api-C412zuay.js +15 -0
  3. package/dist/assets/app-manager-provider-Cm-KiZZG.js +1 -0
  4. package/dist/assets/app-navigation.config-BORqHkbN.js +1 -0
  5. package/dist/assets/{book-open-CVEuA0y5.js → book-open-DgLqYpNY.js} +1 -1
  6. package/dist/assets/{channels-list-page-BqhqaBf1.js → channels-list-page-sISO_4Yj.js} +2 -2
  7. package/dist/assets/{chat-D4KecKjB.js → chat-ChCu7LQD.js} +13 -12
  8. package/dist/assets/chat-page-BCaNZJGT.js +1 -0
  9. package/dist/assets/{chunk-JZWAC4HX-24FLdHl7.js → chunk-JZWAC4HX-DvbcIVPf.js} +1 -1
  10. package/dist/assets/{config-split-page-BGjVACdO.js → config-split-page-BMRGuCJQ.js} +1 -1
  11. package/dist/assets/{createLucideIcon-PPrXCGK8.js → createLucideIcon-BZkY6emz.js} +1 -1
  12. package/dist/assets/desktop-update-config-BfJ5iSeY.js +1 -0
  13. package/dist/assets/{dialog-CTCX7oLf.js → dialog-B-CXiFPZ.js} +1 -1
  14. package/dist/assets/{dist-FL5e8mMi.js → dist-DYVfg3q5.js} +1 -1
  15. package/dist/assets/{doc-browser-C02neCIE.js → doc-browser-BUlCkZo2.js} +1 -1
  16. package/dist/assets/doc-browser-CzCV73NJ.js +1 -0
  17. package/dist/assets/doc-browser-Doh2541x.js +1 -0
  18. package/dist/assets/{doc-browser-context-C-WPOji4.js → doc-browser-context-DfLHAWbG.js} +1 -1
  19. package/dist/assets/{es2015-BNy4R8AC.js → es2015-BXroVnPi.js} +1 -1
  20. package/dist/assets/{external-link-BNtqJE01.js → external-link-Sw3ah_JD.js} +1 -1
  21. package/dist/assets/{folder-QyJHVUNz.js → folder-D7-VTnkz.js} +1 -1
  22. package/dist/assets/{hash-BGYUE-zr.js → hash-zajSTDXZ.js} +1 -1
  23. package/dist/assets/i18n-C5Mibli1.js +1 -0
  24. package/dist/assets/index-CUmk8xFK.css +1 -0
  25. package/dist/assets/index-CqPDhosM.js +2 -0
  26. package/dist/assets/{key-round-DenCfA2w.js → key-round-CnI1mc9F.js} +1 -1
  27. package/dist/assets/loader-circle-B5i8oMMY.js +1 -0
  28. package/dist/assets/{logo-badge-CKAxvQFc.js → logo-badge-BQgKnVtz.js} +1 -1
  29. package/dist/assets/{logos-CqXnaJIm.js → logos-CqVm0q0W.js} +1 -1
  30. package/dist/assets/marketplace-page-C8uaWkfd.js +1 -0
  31. package/dist/assets/{marketplace-page-XnDa2ulT.js → marketplace-page-C9oZ01rM.js} +2 -2
  32. package/dist/assets/mcp-marketplace-page-DuEixgSs.js +40 -0
  33. package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +1 -0
  34. package/dist/assets/message-square-D6Z4NwpG.js +1 -0
  35. package/dist/assets/{model-config-ByeL6Toe.js → model-config-mfhqEZBG.js} +1 -1
  36. package/dist/assets/{notice-card-D00-02yg.js → notice-card-CozHB03G.js} +1 -1
  37. package/dist/assets/play-D8WJLnJe.js +1 -0
  38. package/dist/assets/plus-Di0KAkiO.js +1 -0
  39. package/dist/assets/{popover-AmJkxio3.js → popover-CPUPma-w.js} +1 -1
  40. package/dist/assets/{provider-scoped-model-input-CfFJsJp-.js → provider-scoped-model-input-CL9sti2I.js} +1 -1
  41. package/dist/assets/{providers-list-HMQzW2WV.js → providers-list-HPmL2akJ.js} +1 -1
  42. package/dist/assets/{refresh-ccw-B-dhb3yS.js → refresh-ccw-Bii4w8aB.js} +1 -1
  43. package/dist/assets/refresh-cw-BxojR62w.js +1 -0
  44. package/dist/assets/remote-oDlAdgVA.js +1 -0
  45. package/dist/assets/{rotate-cw-BWqAG3Fv.js → rotate-cw-1Xqa7LZ8.js} +1 -1
  46. package/dist/assets/runtime-config-page-BCshTAAE.js +1 -0
  47. package/dist/assets/{save-DpdkGieJ.js → save--BVI5wZX.js} +1 -1
  48. package/dist/assets/search-config-Bcnk9VlL.js +1 -0
  49. package/dist/assets/{search-CQUdr7j_.js → search-vChioOoe.js} +1 -1
  50. package/dist/assets/{secrets-config-YCsGd1am.js → secrets-config-Dde-5Y1w.js} +2 -2
  51. package/dist/assets/{select-DVUtSFHZ.js → select-BELPuXLW.js} +1 -1
  52. package/dist/assets/{sessions-config-page-BKN-XdKr.js → sessions-config-page-CG49_0Z6.js} +2 -2
  53. package/dist/assets/{setting-row-Cb5-lFs-.js → setting-row-D5DtT6Ny.js} +1 -1
  54. package/dist/assets/{settings-DgtZZlnF.js → settings-CiRChctQ.js} +1 -1
  55. package/dist/assets/skeleton-CFQRIUzt.js +1 -0
  56. package/dist/assets/{sparkles-DNSCyDhL.js → sparkles-D1ZKWdm4.js} +1 -1
  57. package/dist/assets/{status-dot-X_j51OfA.js → status-dot-Dv_hiUVa.js} +1 -1
  58. package/dist/assets/{tabs-custom-CcWmekaF.js → tabs-custom-CsACkVji.js} +1 -1
  59. package/dist/assets/{tag-chip-fdbK2wE6.js → tag-chip-D9BWWgYg.js} +1 -1
  60. package/dist/assets/theme-provider-DeBrTglS.js +1 -0
  61. package/dist/assets/{tooltip-BkZCQcKw.js → tooltip-CI0rpNee.js} +1 -1
  62. package/dist/assets/{trash-2-CqciSCsg.js → trash-2-rY9ZteZX.js} +1 -1
  63. package/dist/assets/use-config-CrWZ_TSF.js +1 -0
  64. package/dist/assets/{use-confirm-dialog-DSrb9205.js → use-confirm-dialog-hbynwWf2.js} +1 -1
  65. package/dist/assets/{use-infinite-scroll-loader-DmowtyTI.js → use-infinite-scroll-loader-Cw5qQr3-.js} +1 -1
  66. package/dist/assets/{use-viewport-layout-CaALCA51.js → use-viewport-layout-CWHVDC6z.js} +1 -1
  67. package/dist/assets/x-DpTzXQcX.js +1 -0
  68. package/dist/index.html +40 -39
  69. package/package.json +7 -6
  70. package/src/app/index.tsx +7 -1
  71. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +1 -1
  72. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -0
  73. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +9 -4
  74. package/src/features/chat/components/conversation/chat-message-list.container.test.tsx +64 -6
  75. package/src/features/chat/components/conversation/chat-message-list.container.tsx +185 -17
  76. package/src/features/chat/components/session/session-context-icon.tsx +1 -4
  77. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +3 -1
  78. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +74 -2
  79. package/src/features/chat/hooks/use-ncp-session-conversation.ts +32 -10
  80. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +20 -0
  81. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +25 -0
  82. package/src/features/chat/managers/ncp-chat-input.manager.ts +5 -1
  83. package/src/features/chat/pages/ncp-chat-page.tsx +15 -11
  84. package/src/features/chat/stores/chat-thread.store.ts +8 -2
  85. package/src/features/chat/utils/chat-context-window-indicator.utils.ts +50 -0
  86. package/src/features/chat/utils/chat-runtime.utils.ts +1 -1
  87. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +165 -0
  88. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.ts +50 -0
  89. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +27 -0
  90. package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -4
  91. package/src/features/chat/utils/ncp-session-context-metadata.utils.ts +121 -0
  92. package/src/features/chat/utils/session-context.utils.ts +1 -2
  93. package/src/features/system-status/components/config/runtime-config-editor.tsx +6 -0
  94. package/src/features/system-status/components/config/runtime-settings-card.tsx +12 -0
  95. package/src/features/system-status/components/desktop-update-config.test.tsx +17 -7
  96. package/src/features/system-status/components/desktop-update-config.tsx +75 -30
  97. package/src/features/system-status/hooks/use-system-status.ts +0 -11
  98. package/src/features/system-status/index.ts +4 -1
  99. package/src/features/system-status/managers/runtime-update.manager.ts +330 -0
  100. package/src/features/system-status/managers/system-status.manager.test.ts +0 -25
  101. package/src/features/system-status/managers/system-status.manager.ts +1 -30
  102. package/src/features/system-status/stores/runtime-update.store.ts +24 -0
  103. package/src/features/system-status/types/system-status.types.ts +0 -2
  104. package/src/features/system-status/utils/runtime-config-agent.utils.ts +6 -1
  105. package/src/features/system-status/utils/system-status.utils.test.ts +1 -85
  106. package/src/features/system-status/utils/system-status.utils.ts +1 -23
  107. package/src/platforms/desktop/managers/desktop-update.manager.ts +6 -0
  108. package/src/platforms/desktop/types/desktop-update.types.ts +21 -19
  109. package/src/shared/components/common/brand-header.test.tsx +142 -0
  110. package/src/shared/components/common/brand-header.tsx +93 -0
  111. package/src/shared/components/cron-config.tsx +1 -1
  112. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +1 -1
  113. package/src/shared/components/doc-browser/doc-browser.tsx +1 -1
  114. package/src/shared/components/search-config.tsx +3 -3
  115. package/src/shared/lib/api/README.md +3 -0
  116. package/src/shared/lib/api/index.ts +2 -0
  117. package/src/shared/lib/api/ncp-attachments.ts +2 -2
  118. package/src/shared/lib/api/ncp-session.types.ts +92 -0
  119. package/src/shared/lib/api/runtime-update.service.ts +50 -0
  120. package/src/shared/lib/api/types.ts +9 -74
  121. package/src/shared/lib/i18n/{chat.ts → chat-labels.utils.ts} +13 -1
  122. package/src/shared/lib/i18n/desktop-update-labels.utils.ts +65 -0
  123. package/src/shared/lib/i18n/index.ts +4 -5
  124. package/src/shared/lib/i18n/runtime/i18n-language-owner.ts +5 -5
  125. package/src/shared/lib/transport/remote-transport.service.ts +1 -1
  126. package/src/shared/lib/ui-document-title/index.ts +1 -1
  127. package/tsconfig.json +1 -0
  128. package/dist/assets/api-BurjmW4A.js +0 -15
  129. package/dist/assets/app-manager-provider-DhxUmyTv.js +0 -1
  130. package/dist/assets/app-navigation.config-Bpd16Pem.js +0 -1
  131. package/dist/assets/chat-page-Cc7n80lW.js +0 -1
  132. package/dist/assets/desktop-update-config-fMLlSStv.js +0 -1
  133. package/dist/assets/doc-browser-COj7x090.js +0 -1
  134. package/dist/assets/doc-browser-fyn7eDTp.js +0 -1
  135. package/dist/assets/i18n-CM4y8Mw9.js +0 -1
  136. package/dist/assets/index-CtVSzMPM.js +0 -2
  137. package/dist/assets/index-N3hjuljD.css +0 -1
  138. package/dist/assets/loader-circle-R23uEPkM.js +0 -1
  139. package/dist/assets/marketplace-page-mF-M5mku.js +0 -1
  140. package/dist/assets/mcp-marketplace-page-BArKWcRZ.js +0 -40
  141. package/dist/assets/mcp-marketplace-page-DBUcIIHJ.js +0 -1
  142. package/dist/assets/message-square-Dm34zD6k.js +0 -1
  143. package/dist/assets/play-ul4L6MWm.js +0 -1
  144. package/dist/assets/plus-D14303DH.js +0 -1
  145. package/dist/assets/remote-B4ELSd3u.js +0 -1
  146. package/dist/assets/runtime-config-page-N4FP6H0M.js +0 -1
  147. package/dist/assets/search-config-B62TY-z2.js +0 -1
  148. package/dist/assets/skeleton-BCPi52jT.js +0 -1
  149. package/dist/assets/theme-provider-WTWq_jYq.js +0 -1
  150. package/dist/assets/use-config-CyvhbRhf.js +0 -1
  151. package/dist/assets/x-tYcSDsrY.js +0 -1
  152. /package/dist/assets/{config-hints-CPNzbMEp.js → config-hints-MogHYQ8G.js} +0 -0
@@ -13,6 +13,10 @@ import {
13
13
  } from "@/features/chat/utils/chat-message.utils";
14
14
  import { readInlineTokensFromMetadata } from "@/features/chat/utils/chat-inline-token.utils";
15
15
  import { adaptNcpMessageToUiMessage } from "@/features/chat/utils/ncp-session-adapter.utils";
16
+ import {
17
+ readContextCompactionTimeline,
18
+ type ContextCompactionTimelineView,
19
+ } from "@/features/chat/utils/ncp-session-context-metadata.utils";
16
20
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
17
21
  import { useI18n } from "@/app/components/i18n-provider";
18
22
  import { formatDateTime, t } from "@/shared/lib/i18n";
@@ -30,6 +34,24 @@ const messageViewModelCache = new WeakMap<
30
34
  { language: string; viewModel: ChatMessageViewModel }
31
35
  >();
32
36
 
37
+ type ChatTimelineItem =
38
+ | {
39
+ kind: "messages";
40
+ key: string;
41
+ messages: ChatMessageViewModel[];
42
+ }
43
+ | {
44
+ kind: "compaction";
45
+ key: string;
46
+ checkpoint: ContextCompactionTimelineView;
47
+ };
48
+
49
+ type TimelineCheckpointPlacement = {
50
+ key: string;
51
+ checkpoint: ContextCompactionTimelineView;
52
+ boundaryIndex: number;
53
+ };
54
+
33
55
  function buildChatMessageAdapterTexts(
34
56
  language: string,
35
57
  ): ChatMessageAdapterTexts {
@@ -84,6 +106,137 @@ function buildChatMessageTexts(language: string) {
84
106
  };
85
107
  }
86
108
 
109
+ function ChatContextCompactionDivider({
110
+ checkpoint,
111
+ }: {
112
+ checkpoint: ContextCompactionTimelineView;
113
+ }) {
114
+ const title = [
115
+ `${t("chatContextCompactionCoveredMessages")}: ${checkpoint.coveredSessionMessageCount}`,
116
+ `${t("chatContextCompactionOriginalTokens")}: ${checkpoint.originalEstimatedTokens}`,
117
+ `${t("chatContextCompactionProjectedTokens")}: ${checkpoint.projectedEstimatedTokens}`,
118
+ ].join("\n");
119
+ return (
120
+ <div className="my-4 flex items-center gap-3 text-[11px] text-gray-500" title={title}>
121
+ <div className="h-px flex-1 bg-gray-200" />
122
+ <div className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-gray-50 px-3 py-1">
123
+ {checkpoint.status === "compressing" ? (
124
+ <span className="h-1.5 w-1.5 rounded-full bg-gray-400 animate-pulse" />
125
+ ) : (
126
+ <span className="h-1.5 w-1.5 rounded-full bg-gray-300" />
127
+ )}
128
+ <span>
129
+ {checkpoint.status === "compressing"
130
+ ? t("chatContextCompactionCompressing")
131
+ : t("chatContextCompactionCompressed")}
132
+ </span>
133
+ </div>
134
+ <div className="h-px flex-1 bg-gray-200" />
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function resolveCompactionBoundaryIndex(params: {
140
+ rawMessages: readonly NcpMessage[];
141
+ normalRawMessages: readonly NcpMessage[];
142
+ rawMessageId: string;
143
+ }): number {
144
+ const {
145
+ normalRawMessages,
146
+ rawMessageId,
147
+ rawMessages,
148
+ } = params;
149
+ const physicalIndex = rawMessages.findIndex(
150
+ (message) => message.id === rawMessageId,
151
+ );
152
+ if (physicalIndex < 0) {
153
+ return normalRawMessages.length - 1;
154
+ }
155
+ const previousNormalCount = rawMessages
156
+ .slice(0, physicalIndex)
157
+ .filter((message) => !readContextCompactionTimeline(message)).length;
158
+ return previousNormalCount - 1;
159
+ }
160
+
161
+ function buildTimelineItems(params: {
162
+ rawMessages: readonly NcpMessage[];
163
+ messages: ChatMessageViewModel[];
164
+ }): ChatTimelineItem[] {
165
+ const normalRawMessages = params.rawMessages.filter(
166
+ (message) => !readContextCompactionTimeline(message),
167
+ );
168
+ const checkpoints = params.rawMessages
169
+ .map((message) => ({
170
+ rawMessageId: message.id,
171
+ checkpoint: readContextCompactionTimeline(message),
172
+ }))
173
+ .filter(
174
+ (entry): entry is { rawMessageId: string; checkpoint: ContextCompactionTimelineView } =>
175
+ Boolean(entry.checkpoint),
176
+ )
177
+ .map((entry) => ({
178
+ key: entry.rawMessageId,
179
+ checkpoint: entry.checkpoint,
180
+ boundaryIndex: resolveCompactionBoundaryIndex({
181
+ rawMessages: params.rawMessages,
182
+ normalRawMessages,
183
+ rawMessageId: entry.rawMessageId,
184
+ }),
185
+ }))
186
+ .sort((left, right) => left.boundaryIndex - right.boundaryIndex);
187
+
188
+ const items: ChatTimelineItem[] = [];
189
+ let pendingMessages: ChatMessageViewModel[] = [];
190
+ let checkpointCursor = 0;
191
+ const flushPendingMessages = (key: string) => {
192
+ if (pendingMessages.length === 0) {
193
+ return;
194
+ }
195
+ items.push({
196
+ kind: "messages",
197
+ key,
198
+ messages: pendingMessages,
199
+ });
200
+ pendingMessages = [];
201
+ };
202
+
203
+ normalRawMessages.forEach((rawMessage, index) => {
204
+ const message = params.messages[index];
205
+ if (message) {
206
+ pendingMessages.push(message);
207
+ }
208
+ while (checkpointCursor < checkpoints.length && checkpoints[checkpointCursor]?.boundaryIndex <= index) {
209
+ const currentCheckpoint = checkpoints[checkpointCursor];
210
+ flushPendingMessages(`messages-before-${currentCheckpoint.key}`);
211
+ items.push({
212
+ kind: "compaction",
213
+ key: currentCheckpoint.key,
214
+ checkpoint: currentCheckpoint.checkpoint,
215
+ });
216
+ checkpointCursor += 1;
217
+ }
218
+ });
219
+ while (checkpointCursor < checkpoints.length) {
220
+ const currentCheckpoint = checkpoints[checkpointCursor];
221
+ flushPendingMessages(`messages-before-${currentCheckpoint.key}`);
222
+ items.push({
223
+ kind: "compaction",
224
+ key: currentCheckpoint.key,
225
+ checkpoint: currentCheckpoint.checkpoint,
226
+ });
227
+ checkpointCursor += 1;
228
+ }
229
+ flushPendingMessages("messages-final");
230
+ if (items.length === 0) {
231
+ items.push({
232
+ kind: "messages",
233
+ key: "messages-empty",
234
+ messages: [],
235
+ });
236
+ }
237
+ return items;
238
+ }
239
+
87
240
  export function ChatMessageListContainer({
88
241
  messages: rawMessages,
89
242
  isSending,
@@ -98,10 +251,13 @@ export function ChatMessageListContainer({
98
251
  );
99
252
 
100
253
  const messages = useMemo(() => {
101
- return rawMessages.map((message) => {
254
+ return rawMessages.flatMap((message) => {
255
+ if (readContextCompactionTimeline(message)) {
256
+ return [];
257
+ }
102
258
  const cached = messageViewModelCache.get(message);
103
259
  if (cached && cached.language === language) {
104
- return cached.viewModel;
260
+ return [cached.viewModel];
105
261
  }
106
262
 
107
263
  const uiMessage = adaptNcpMessageToUiMessage(message);
@@ -121,7 +277,7 @@ export function ChatMessageListContainer({
121
277
  });
122
278
 
123
279
  messageViewModelCache.set(message, { language, viewModel });
124
- return viewModel;
280
+ return [viewModel];
125
281
  });
126
282
  }, [language, rawMessages, texts]);
127
283
 
@@ -138,22 +294,34 @@ export function ChatMessageListContainer({
138
294
  () => buildChatMessageTexts(language),
139
295
  [language],
140
296
  );
297
+ const timelineItems = useMemo(
298
+ () => buildTimelineItems({ rawMessages, messages }),
299
+ [messages, rawMessages],
300
+ );
141
301
 
142
302
  return (
143
- <ChatMessageList
144
- messages={messages}
145
- isSending={isSending}
146
- hasAssistantDraft={hasAssistantDraft}
147
- className={className}
148
- texts={messageTexts}
149
- onToolAction={onToolAction}
150
- onFileOpen={onFileOpen}
151
- renderToolAgent={(agentId) => (
152
- <AgentIdentityAvatar
153
- agentId={agentId}
154
- className="h-4 w-4 shrink-0"
155
- />
303
+ <div className={className}>
304
+ {timelineItems.map((item, index) =>
305
+ item.kind === "compaction" ? (
306
+ <ChatContextCompactionDivider key={item.key} checkpoint={item.checkpoint} />
307
+ ) : (
308
+ <ChatMessageList
309
+ key={item.key}
310
+ messages={item.messages}
311
+ isSending={index === timelineItems.length - 1 ? isSending : false}
312
+ hasAssistantDraft={hasAssistantDraft}
313
+ texts={messageTexts}
314
+ onToolAction={onToolAction}
315
+ onFileOpen={onFileOpen}
316
+ renderToolAgent={(agentId) => (
317
+ <AgentIdentityAvatar
318
+ agentId={agentId}
319
+ className="h-4 w-4 shrink-0"
320
+ />
321
+ )}
322
+ />
323
+ ),
156
324
  )}
157
- />
325
+ </div>
158
326
  );
159
327
  }
@@ -3,7 +3,7 @@ import { resolveAppResourceUri } from '@/shared/lib/app-resource-uri';
3
3
  import { LogoBadge } from '@/shared/components/common/logo-badge';
4
4
  import { getChannelLogo } from '@/shared/lib/logos';
5
5
  import { cn } from '@/shared/lib/utils';
6
- import { AlarmClock, Bot, HeartPulse } from 'lucide-react';
6
+ import { AlarmClock, Bot } from 'lucide-react';
7
7
 
8
8
  export function SessionContextIconNode({ icon, className }: { icon: SessionContextIcon; className?: string }) {
9
9
  if (icon.kind === 'channel-logo') {
@@ -21,9 +21,6 @@ export function SessionContextIconNode({ icon, className }: { icon: SessionConte
21
21
  />
22
22
  );
23
23
  }
24
- if (icon.icon === 'heartbeat') {
25
- return <HeartPulse className={cn('h-3.5 w-3.5', className)} />;
26
- }
27
24
  return <AlarmClock className={cn('h-3.5 w-3.5', className)} />;
28
25
  }
29
26
 
@@ -13,6 +13,7 @@ import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
13
13
  import type { ChatChildSessionTab } from '@/features/chat/stores/chat-thread.store';
14
14
  import type { ChatSessionTypeOption } from '@/features/chat/hooks/use-chat-session-type-state';
15
15
  import { resolveSessionTypeLabel } from '@/features/chat/hooks/use-chat-session-type-state';
16
+ import { readNcpContextWindowValue } from '@/features/chat/utils/ncp-session-context-metadata.utils';
16
17
 
17
18
  function buildChildSessionTabs(params: {
18
19
  parentSessionKey: string | null;
@@ -121,7 +122,7 @@ export function useNcpChatSnapshotSync(params: {
121
122
  effectiveSessionProjectName: string | null;
122
123
  selectedSession: SessionEntryView | null;
123
124
  threadRef: MutableRefObject<HTMLDivElement | null>;
124
- agent: Pick<UseHydratedNcpAgentResult, 'isHydrating' | 'visibleMessages'>;
125
+ agent: Pick<UseHydratedNcpAgentResult, 'isHydrating' | 'snapshot' | 'visibleMessages'>;
125
126
  isAwaitingAssistantOutput: boolean;
126
127
  parentSession: SessionEntryView | null;
127
128
  childSessionTabs: ChatChildSessionTab[];
@@ -165,6 +166,7 @@ export function useNcpChatSnapshotSync(params: {
165
166
  messages: params.agent.visibleMessages,
166
167
  isSending: params.isSending,
167
168
  isAwaitingAssistantOutput: params.isAwaitingAssistantOutput,
169
+ contextWindow: readNcpContextWindowValue(params.agent.snapshot.contextWindow),
168
170
  parentSessionKey: params.parentSession?.key ?? null,
169
171
  parentSessionLabel: params.parentSession
170
172
  ? sessionDisplayName(params.parentSession)
@@ -1,5 +1,6 @@
1
1
  import { act, renderHook, waitFor } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type * as SharedApi from "@/shared/lib/api";
3
4
  import { fetchNcpSessionConversationSeed, useNcpSessionConversation } from "./use-ncp-session-conversation";
4
5
 
5
6
  const mocks = vi.hoisted(() => ({
@@ -30,7 +31,7 @@ const mocks = vi.hoisted(() => ({
30
31
  }));
31
32
 
32
33
  vi.mock("@/shared/lib/api", async (importOriginal) => {
33
- const actual = await importOriginal<typeof import("@/shared/lib/api")>();
34
+ const actual = await importOriginal<typeof SharedApi>();
34
35
  return {
35
36
  ...actual,
36
37
  fetchNcpSessionMessages: mocks.fetchNcpSessionMessages,
@@ -51,7 +52,13 @@ vi.mock("@nextclaw/ncp-http-agent-client", () => ({
51
52
  }));
52
53
 
53
54
  vi.mock("@/features/system-status", () => ({
54
- useChatRuntimeAvailability: vi.fn(() => mocks.runtimeAvailability),
55
+ useSystemStatus: vi.fn(() => ({
56
+ ...mocks.runtimeAvailability,
57
+ lifecyclePhase: mocks.runtimeAvailability.phase,
58
+ activeSystemAction: null,
59
+ bootstrapStatus: null,
60
+ lastError: null,
61
+ })),
55
62
  }));
56
63
 
57
64
  describe("useNcpSessionConversation", () => {
@@ -70,6 +77,19 @@ describe("useNcpSessionConversation", () => {
70
77
  status: "running",
71
78
  total: 1,
72
79
  messages: [{ id: "msg-1" }],
80
+ contextWindow: {
81
+ usedContextTokens: 42,
82
+ totalContextTokens: 100,
83
+ prunedUsedContextTokens: 42,
84
+ availableContextTokens: 58,
85
+ droppedHistoryCount: 0,
86
+ truncatedToolResultCount: 0,
87
+ truncatedSystemPrompt: false,
88
+ truncatedUserMessage: false,
89
+ compacted: false,
90
+ compactedMessageCount: 0,
91
+ updatedAt: "2026-05-05T00:00:00.000Z",
92
+ },
73
93
  });
74
94
 
75
95
  const result = await fetchNcpSessionConversationSeed(
@@ -82,6 +102,19 @@ describe("useNcpSessionConversation", () => {
82
102
  expect(result).toEqual({
83
103
  messages: [{ id: "msg-1" }],
84
104
  status: "running",
105
+ contextWindow: {
106
+ usedContextTokens: 42,
107
+ totalContextTokens: 100,
108
+ prunedUsedContextTokens: 42,
109
+ availableContextTokens: 58,
110
+ droppedHistoryCount: 0,
111
+ truncatedToolResultCount: 0,
112
+ truncatedSystemPrompt: false,
113
+ truncatedUserMessage: false,
114
+ compacted: false,
115
+ compactedMessageCount: 0,
116
+ updatedAt: "2026-05-05T00:00:00.000Z",
117
+ },
85
118
  });
86
119
  });
87
120
 
@@ -113,6 +146,45 @@ describe("useNcpSessionConversation", () => {
113
146
  expect(mocks.hydratedCalls[1]?.client).toBe(mocks.clientInstances[1]);
114
147
  });
115
148
 
149
+ it("exposes the hydrated session context window without changing the generic ncp agent seed", async () => {
150
+ const contextWindow = {
151
+ usedContextTokens: 42,
152
+ totalContextTokens: 100,
153
+ prunedUsedContextTokens: 42,
154
+ availableContextTokens: 58,
155
+ droppedHistoryCount: 0,
156
+ truncatedToolResultCount: 0,
157
+ truncatedSystemPrompt: false,
158
+ truncatedUserMessage: false,
159
+ compacted: false,
160
+ compactedMessageCount: 0,
161
+ updatedAt: "2026-05-05T00:00:00.000Z",
162
+ };
163
+ mocks.fetchNcpSessionMessages.mockResolvedValue({
164
+ sessionId: "session-1",
165
+ status: "idle",
166
+ total: 0,
167
+ messages: [],
168
+ contextWindow,
169
+ });
170
+
171
+ const { result, rerender } = renderHook(() => useNcpSessionConversation("session-1"));
172
+ const loadSeed = mocks.hydratedCalls[0]?.loadSeed as (
173
+ sessionId: string,
174
+ signal: AbortSignal
175
+ ) => Promise<{ messages: unknown[]; status: string }>;
176
+
177
+ await act(async () => {
178
+ await expect(loadSeed("session-1", new AbortController().signal)).resolves.toEqual({
179
+ messages: [],
180
+ status: "idle",
181
+ });
182
+ });
183
+ rerender();
184
+
185
+ expect(result.current.snapshot.contextWindow).toEqual(contextWindow);
186
+ });
187
+
116
188
  it("retries hydration once the runtime becomes ready after a startup placeholder error", async () => {
117
189
  mocks.useHydratedNcpAgent.mockImplementation(() => ({
118
190
  snapshot: {
@@ -1,9 +1,9 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
3
3
  import { useHydratedNcpAgent, type NcpConversationSeed } from "@nextclaw/ncp-react";
4
- import { API_BASE, fetchNcpSessionMessages } from "@/shared/lib/api";
4
+ import { API_BASE, fetchNcpSessionMessages, type SessionContextWindowView } from "@/shared/lib/api";
5
5
  import { createNcpAppClientFetch } from "@/features/chat/utils/ncp-app-client-fetch.utils";
6
- import { useChatRuntimeAvailability } from "@/features/system-status";
6
+ import { useSystemStatus } from "@/features/system-status";
7
7
 
8
8
  const DEFAULT_MESSAGE_LIMIT = 300;
9
9
  const NCP_AGENT_UNAVAILABLE_DURING_STARTUP = "ncp agent unavailable during startup";
@@ -12,6 +12,10 @@ type UseNcpSessionConversationOptions = {
12
12
  messageLimit?: number;
13
13
  };
14
14
 
15
+ type NcpConversationSeedWithContextWindow = NcpConversationSeed & {
16
+ contextWindow?: SessionContextWindowView | null;
17
+ };
18
+
15
19
  function isMissingNcpSessionError(error: unknown): boolean {
16
20
  if (!(error instanceof Error)) {
17
21
  return false;
@@ -40,7 +44,7 @@ export async function fetchNcpSessionConversationSeed(
40
44
  sessionId: string,
41
45
  signal: AbortSignal,
42
46
  messageLimit = DEFAULT_MESSAGE_LIMIT,
43
- ): Promise<NcpConversationSeed> {
47
+ ): Promise<NcpConversationSeedWithContextWindow> {
44
48
  signal.throwIfAborted();
45
49
 
46
50
  try {
@@ -49,6 +53,7 @@ export async function fetchNcpSessionConversationSeed(
49
53
  return {
50
54
  messages: response.messages,
51
55
  status: response.status ?? "idle",
56
+ contextWindow: response.contextWindow ?? null,
52
57
  };
53
58
  } catch (error) {
54
59
  signal.throwIfAborted();
@@ -92,13 +97,24 @@ export function useNcpSessionConversation(
92
97
  options: UseNcpSessionConversationOptions = {},
93
98
  ) {
94
99
  const [client] = useState(() => createNcpSessionConversationClient());
95
- const runtimeAvailability = useChatRuntimeAvailability();
100
+ const systemStatus = useSystemStatus();
96
101
  const [hydrationRetryVersion, setHydrationRetryVersion] = useState(0);
102
+ const [seedContextWindow, setSeedContextWindow] = useState<SessionContextWindowView | null>(null);
97
103
  const messageLimit = options.messageLimit ?? DEFAULT_MESSAGE_LIMIT;
104
+ useEffect(() => {
105
+ setSeedContextWindow(null);
106
+ }, [sessionId]);
98
107
  const loadSeed = useCallback(
99
- (targetSessionId: string, signal: AbortSignal) => {
108
+ async (targetSessionId: string, signal: AbortSignal) => {
100
109
  void hydrationRetryVersion;
101
- return fetchNcpSessionConversationSeed(targetSessionId, signal, messageLimit);
110
+ const seed = await fetchNcpSessionConversationSeed(targetSessionId, signal, messageLimit);
111
+ if (!signal.aborted) {
112
+ setSeedContextWindow(seed.contextWindow ?? null);
113
+ }
114
+ return {
115
+ messages: seed.messages,
116
+ status: seed.status,
117
+ };
102
118
  },
103
119
  [hydrationRetryVersion, messageLimit],
104
120
  );
@@ -110,12 +126,18 @@ export function useNcpSessionConversation(
110
126
  const currentAgentError =
111
127
  agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
112
128
  const readyRetrySignature =
113
- runtimeAvailability.phase === "ready" &&
129
+ systemStatus.phase === "ready" &&
114
130
  isNcpAgentStartupUnavailableErrorMessage(currentAgentError)
115
- ? `${sessionId}:${runtimeAvailability.lastReadyAt ?? 0}`
131
+ ? `${sessionId}:${systemStatus.lastReadyAt ?? 0}`
116
132
  : null;
117
133
  useSyncReadyRetryVersion(readyRetrySignature, () => {
118
134
  setHydrationRetryVersion((current) => current + 1);
119
135
  });
120
- return agent;
136
+ return useMemo(() => ({
137
+ ...agent,
138
+ snapshot: {
139
+ ...agent.snapshot,
140
+ contextWindow: agent.snapshot.contextWindow ?? seedContextWindow,
141
+ },
142
+ }), [agent, seedContextWindow]);
121
143
  }
@@ -0,0 +1,20 @@
1
+ import { useMemo } from 'react';
2
+ import type { ChatContextWindowIndicator } from '@nextclaw/agent-chat-ui';
3
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
4
+ import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
5
+ import { buildChatContextWindowIndicator } from '@/features/chat/utils/chat-context-window-indicator.utils';
6
+
7
+ export function useSelectedSessionContextWindowIndicator(): ChatContextWindowIndicator | null {
8
+ const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
9
+ const draftSessionKey = useChatSessionListStore((state) => state.snapshot.draftSessionKey);
10
+ const liveSessionKey = useChatThreadStore((state) => state.snapshot.sessionKey);
11
+ const liveContextWindow = useChatThreadStore((state) => state.snapshot.contextWindow);
12
+ const currentSessionKey = selectedSessionKey ?? draftSessionKey;
13
+
14
+ return useMemo(() => {
15
+ if (liveSessionKey === currentSessionKey && liveContextWindow) {
16
+ return buildChatContextWindowIndicator(liveContextWindow);
17
+ }
18
+ return null;
19
+ }, [currentSessionKey, liveContextWindow, liveSessionKey]);
20
+ }
@@ -33,6 +33,24 @@ describe('NcpChatInputManager', () => {
33
33
  state: {
34
34
  ...useSystemStatusStore.getState().state,
35
35
  lifecyclePhase: 'ready',
36
+ bootstrapStatus: {
37
+ phase: 'ready',
38
+ ncpAgent: {
39
+ state: 'ready',
40
+ },
41
+ pluginHydration: {
42
+ state: 'ready',
43
+ loadedPluginCount: 1,
44
+ totalPluginCount: 1,
45
+ },
46
+ channels: {
47
+ state: 'ready',
48
+ enabled: [],
49
+ },
50
+ remote: {
51
+ state: 'pending',
52
+ },
53
+ },
36
54
  },
37
55
  });
38
56
  useChatSessionListStore.setState({
@@ -125,6 +143,13 @@ describe('NcpChatInputManager', () => {
125
143
  state: {
126
144
  ...useSystemStatusStore.getState().state,
127
145
  lifecyclePhase: 'cold-starting',
146
+ bootstrapStatus: {
147
+ ...useSystemStatusStore.getState().state.bootstrapStatus!,
148
+ phase: 'kernel-starting',
149
+ ncpAgent: {
150
+ state: 'pending',
151
+ },
152
+ },
128
153
  },
129
154
  });
130
155
  const streamActionsManager = {
@@ -22,6 +22,7 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
22
22
  import type { ChatSessionListManager } from '@/features/chat/managers/chat-session-list.manager';
23
23
  import { ChatSessionPreferenceSync } from '@/features/chat/managers/chat-session-preference-sync.manager';
24
24
  import { isNcpChatSendDisabled } from '@/features/chat/utils/ncp-chat-input-availability.utils';
25
+ import { isNcpChatRuntimeBlocked } from '@/features/chat/utils/ncp-chat-runtime-availability.utils';
25
26
  import { chatRecentModelsManager } from '@/features/chat/managers/chat-recent-models.manager';
26
27
  import { chatRecentSkillsManager } from '@/features/chat/managers/chat-recent-skills.manager';
27
28
  import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
@@ -67,6 +68,9 @@ export class NcpChatInputManager {
67
68
  private isSameStringArray = (left: string[], right: string[]): boolean =>
68
69
  left.length === right.length && left.every((value, index) => value === right[index]);
69
70
 
71
+ private isRuntimeBlockedForSend = (): boolean =>
72
+ isNcpChatRuntimeBlocked(systemStatusManager.getStatusView());
73
+
70
74
  private syncComposerSnapshot = (nodes: ChatComposerNode[]) => {
71
75
  const currentAttachments = useChatInputStore.getState().snapshot.attachments;
72
76
  const attachments = pruneComposerAttachments(nodes, currentAttachments);
@@ -184,7 +188,7 @@ export class NcpChatInputManager {
184
188
  isNcpChatSendDisabled({
185
189
  snapshot: inputSnapshot,
186
190
  hasSendableDraft: hasSendableContent,
187
- isRuntimeBlocked: systemStatusManager.isChatInteractionBlocked(),
191
+ isRuntimeBlocked: this.isRuntimeBlockedForSend(),
188
192
  })
189
193
  ) {
190
194
  return;
@@ -32,10 +32,8 @@ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-lis
32
32
  import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
33
33
  import { useAgents } from "@/shared/hooks/use-agents";
34
34
  import { normalizeRequestedSkills } from "@/features/chat/utils/chat-runtime.utils";
35
- import {
36
- systemStatusManager,
37
- useChatRuntimeAvailability,
38
- } from "@/features/system-status";
35
+ import { useSystemStatus } from "@/features/system-status";
36
+ import { isNcpChatRuntimeBlocked, resolveNcpChatSendErrorMessage } from "@/features/chat/utils/ncp-chat-runtime-availability.utils";
39
37
  import {
40
38
  getSessionProjectName,
41
39
  normalizeSessionProjectRootValue,
@@ -126,7 +124,8 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
126
124
  const currentSelectedModel = useChatInputStore(
127
125
  (state) => state.snapshot.selectedModel,
128
126
  );
129
- const runtimeAvailability = useChatRuntimeAvailability();
127
+ const systemStatus = useSystemStatus();
128
+ const isRuntimeBlocked = isNcpChatRuntimeBlocked(systemStatus);
130
129
  const agentsQuery = useAgents();
131
130
  const { confirm, ConfirmDialog } = useConfirmDialog();
132
131
  const location = useLocation();
@@ -163,7 +162,8 @@ function useNcpChatPageBaseState(presenter: NcpChatPresenter) {
163
162
  selectedAgentId,
164
163
  pendingProjectRoot,
165
164
  pendingProjectRootSessionKey,
166
- runtimeAvailability,
165
+ systemStatus,
166
+ isRuntimeBlocked,
167
167
  agentsQuery,
168
168
  confirm,
169
169
  ConfirmDialog,
@@ -186,7 +186,8 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
186
186
  agentsQuery,
187
187
  hasSessionProjectRootOverride,
188
188
  pendingProjectRoot,
189
- runtimeAvailability,
189
+ isRuntimeBlocked,
190
+ systemStatus,
190
191
  selectedAgentId,
191
192
  selectedSession,
192
193
  selectedSessionType,
@@ -204,7 +205,7 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
204
205
  const rawLastSendError =
205
206
  agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
206
207
  const filteredLastSendError =
207
- runtimeAvailability.phase === "ready" &&
208
+ systemStatus.phase === "ready" &&
208
209
  isNcpAgentStartupUnavailableErrorMessage(rawLastSendError)
209
210
  ? null
210
211
  : rawLastSendError;
@@ -231,11 +232,14 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
231
232
  canStopCurrentRun: agent.isRunning,
232
233
  stopDisabledReason: agent.isRunning ? null : "__preparing__",
233
234
  lastSendError:
234
- runtimeAvailability.isBlocked
235
+ isRuntimeBlocked
235
236
  ? null
236
- : runtimeAvailability.phase === "ready"
237
+ : systemStatus.phase === "ready"
237
238
  ? filteredLastSendError
238
- : systemStatusManager.getDisplayMessage(filteredLastSendError),
239
+ : resolveNcpChatSendErrorMessage({
240
+ message: filteredLastSendError,
241
+ status: systemStatus,
242
+ }),
239
243
  ...derivedState,
240
244
  };
241
245
  }
@@ -2,7 +2,11 @@ import { create } from 'zustand';
2
2
  import type { MutableRefObject } from 'react';
3
3
  import type { NcpMessage } from '@nextclaw/ncp';
4
4
  import type { ChatFileOperationLineViewModel } from '@nextclaw/agent-chat-ui';
5
- import type { AgentProfileView, SessionTypeIconView } from '@/shared/lib/api';
5
+ import type {
6
+ AgentProfileView,
7
+ SessionContextWindowView,
8
+ SessionTypeIconView
9
+ } from '@/shared/lib/api';
6
10
  import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
7
11
 
8
12
  export type ChatChildSessionTab = {
@@ -58,6 +62,7 @@ export type ChatThreadSnapshot = {
58
62
  activeChildSessionKey?: string | null;
59
63
  workspaceFileTabs: ChatWorkspaceFileTab[];
60
64
  activeWorkspaceFileKey?: string | null;
65
+ contextWindow?: SessionContextWindowView | null;
61
66
  };
62
67
 
63
68
  type ChatThreadStore = {
@@ -93,7 +98,8 @@ const initialSnapshot: ChatThreadSnapshot = {
93
98
  childSessionTabs: [],
94
99
  activeChildSessionKey: null,
95
100
  workspaceFileTabs: [],
96
- activeWorkspaceFileKey: null
101
+ activeWorkspaceFileKey: null,
102
+ contextWindow: null
97
103
  };
98
104
 
99
105
  export const useChatThreadStore = create<ChatThreadStore>((set) => ({