@nextclaw/ui 0.12.19 → 0.12.20-beta.1
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.
- package/CHANGELOG.md +39 -0
- package/dist/assets/api-BcqDx0tm.js +15 -0
- package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
- package/dist/assets/app-navigation.config-CMoWvFEI.js +1 -0
- package/dist/assets/{book-open-CVEuA0y5.js → book-open-DgLqYpNY.js} +1 -1
- package/dist/assets/{channels-list-page-BqhqaBf1.js → channels-list-page-CsoI4OJm.js} +2 -2
- package/dist/assets/{chat-D4KecKjB.js → chat-CA3aRmhx.js} +13 -12
- package/dist/assets/chat-page-gdSN6Pr6.js +1 -0
- package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
- package/dist/assets/{config-split-page-BGjVACdO.js → config-split-page-BMRGuCJQ.js} +1 -1
- package/dist/assets/{createLucideIcon-PPrXCGK8.js → createLucideIcon-BZkY6emz.js} +1 -1
- package/dist/assets/desktop-update-config-CD6-2PfI.js +1 -0
- package/dist/assets/{dialog-CTCX7oLf.js → dialog-csshWetU.js} +1 -1
- package/dist/assets/{dist-FL5e8mMi.js → dist-Bl94Ahwx.js} +1 -1
- package/dist/assets/{doc-browser-C02neCIE.js → doc-browser-BUlCkZo2.js} +1 -1
- package/dist/assets/doc-browser-CzCV73NJ.js +1 -0
- package/dist/assets/doc-browser-Doh2541x.js +1 -0
- package/dist/assets/{doc-browser-context-C-WPOji4.js → doc-browser-context-DfLHAWbG.js} +1 -1
- package/dist/assets/{es2015-BNy4R8AC.js → es2015-JCM5-KtW.js} +1 -1
- package/dist/assets/{external-link-BNtqJE01.js → external-link-Sw3ah_JD.js} +1 -1
- package/dist/assets/{folder-QyJHVUNz.js → folder-D7-VTnkz.js} +1 -1
- package/dist/assets/{hash-BGYUE-zr.js → hash-zajSTDXZ.js} +1 -1
- package/dist/assets/i18n-C5Mibli1.js +1 -0
- package/dist/assets/index-BTDFuKka.js +2 -0
- package/dist/assets/index-CUmk8xFK.css +1 -0
- package/dist/assets/{key-round-DenCfA2w.js → key-round-CnI1mc9F.js} +1 -1
- package/dist/assets/loader-circle-B5i8oMMY.js +1 -0
- package/dist/assets/{logo-badge-CKAxvQFc.js → logo-badge-BQgKnVtz.js} +1 -1
- package/dist/assets/{logos-CqXnaJIm.js → logos-CqVm0q0W.js} +1 -1
- package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
- package/dist/assets/{marketplace-page-XnDa2ulT.js → marketplace-page-DxlxHCFm.js} +2 -2
- package/dist/assets/mcp-marketplace-page-5UjYRWOR.js +40 -0
- package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
- package/dist/assets/message-square-D6Z4NwpG.js +1 -0
- package/dist/assets/{model-config-ByeL6Toe.js → model-config-PccJ9XyH.js} +1 -1
- package/dist/assets/{notice-card-D00-02yg.js → notice-card-CCgk6FvF.js} +1 -1
- package/dist/assets/play-D8WJLnJe.js +1 -0
- package/dist/assets/plus-Di0KAkiO.js +1 -0
- package/dist/assets/{popover-AmJkxio3.js → popover-YAsxDBhY.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CfFJsJp-.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
- package/dist/assets/{providers-list-HMQzW2WV.js → providers-list-8qDMER8o.js} +1 -1
- package/dist/assets/{refresh-ccw-B-dhb3yS.js → refresh-ccw-Bii4w8aB.js} +1 -1
- package/dist/assets/refresh-cw-BxojR62w.js +1 -0
- package/dist/assets/remote-D4TtLPAp.js +1 -0
- package/dist/assets/{rotate-cw-BWqAG3Fv.js → rotate-cw-1Xqa7LZ8.js} +1 -1
- package/dist/assets/runtime-config-page-D-4c5H5z.js +1 -0
- package/dist/assets/{save-DpdkGieJ.js → save--BVI5wZX.js} +1 -1
- package/dist/assets/search-config-D3a65l3r.js +1 -0
- package/dist/assets/{search-CQUdr7j_.js → search-vChioOoe.js} +1 -1
- package/dist/assets/{secrets-config-YCsGd1am.js → secrets-config-CoMlR_7i.js} +2 -2
- package/dist/assets/{select-DVUtSFHZ.js → select-DIZrwsKU.js} +1 -1
- package/dist/assets/{sessions-config-page-BKN-XdKr.js → sessions-config-page-Cc0TJStn.js} +2 -2
- package/dist/assets/{setting-row-Cb5-lFs-.js → setting-row-DiQyrE81.js} +1 -1
- package/dist/assets/{settings-DgtZZlnF.js → settings-CiRChctQ.js} +1 -1
- package/dist/assets/skeleton-CFQRIUzt.js +1 -0
- package/dist/assets/{sparkles-DNSCyDhL.js → sparkles-D1ZKWdm4.js} +1 -1
- package/dist/assets/{status-dot-X_j51OfA.js → status-dot-Dv_hiUVa.js} +1 -1
- package/dist/assets/{tabs-custom-CcWmekaF.js → tabs-custom-CsACkVji.js} +1 -1
- package/dist/assets/{tag-chip-fdbK2wE6.js → tag-chip-C3wDBe_-.js} +1 -1
- package/dist/assets/theme-provider-aOmrJ9J6.js +1 -0
- package/dist/assets/{tooltip-BkZCQcKw.js → tooltip-Dq5Xehpk.js} +1 -1
- package/dist/assets/{trash-2-CqciSCsg.js → trash-2-rY9ZteZX.js} +1 -1
- package/dist/assets/use-config-BQJjq1mP.js +1 -0
- package/dist/assets/{use-confirm-dialog-DSrb9205.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-DmowtyTI.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CaALCA51.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
- package/dist/assets/x-DpTzXQcX.js +1 -0
- package/dist/index.html +40 -39
- package/package.json +9 -6
- package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
- package/src/app/index.tsx +7 -1
- package/src/features/channels/components/config/channel-form.tsx +3 -3
- package/src/features/channels/components/config/weixin-channel-auth-section.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -0
- package/src/features/chat/components/conversation/chat-input-bar.container.tsx +9 -4
- package/src/features/chat/components/conversation/chat-message-list.container.test.tsx +64 -6
- package/src/features/chat/components/conversation/chat-message-list.container.tsx +185 -17
- package/src/features/chat/components/session/session-context-icon.tsx +1 -4
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +3 -1
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +74 -2
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +32 -10
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +20 -0
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +25 -0
- package/src/features/chat/managers/ncp-chat-input.manager.ts +5 -1
- package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
- package/src/features/chat/pages/ncp-chat-page.tsx +15 -11
- package/src/features/chat/stores/chat-thread.store.ts +8 -2
- package/src/features/chat/utils/chat-context-window-indicator.utils.ts +50 -0
- package/src/features/chat/utils/chat-runtime.utils.ts +1 -1
- package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
- package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
- package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +165 -0
- package/src/features/chat/utils/ncp-chat-runtime-availability.utils.ts +50 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +27 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -4
- package/src/features/chat/utils/ncp-session-context-metadata.utils.ts +121 -0
- package/src/features/chat/utils/session-context.utils.ts +1 -2
- package/src/features/system-status/components/config/runtime-config-editor.tsx +6 -0
- package/src/features/system-status/components/config/runtime-settings-card.tsx +12 -0
- package/src/features/system-status/components/desktop-update-config.test.tsx +17 -7
- package/src/features/system-status/components/desktop-update-config.tsx +75 -30
- package/src/features/system-status/hooks/use-system-status.ts +0 -11
- package/src/features/system-status/index.ts +4 -1
- package/src/features/system-status/managers/runtime-update.manager.ts +330 -0
- package/src/features/system-status/managers/system-status.manager.test.ts +0 -25
- package/src/features/system-status/managers/system-status.manager.ts +1 -30
- package/src/features/system-status/stores/runtime-update.store.ts +24 -0
- package/src/features/system-status/types/system-status.types.ts +0 -2
- package/src/features/system-status/utils/runtime-config-agent.utils.ts +6 -1
- package/src/features/system-status/utils/system-status.utils.test.ts +1 -85
- package/src/features/system-status/utils/system-status.utils.ts +1 -23
- package/src/platforms/desktop/managers/desktop-update.manager.ts +6 -0
- package/src/platforms/desktop/types/desktop-update.types.ts +21 -19
- package/src/shared/components/common/brand-header.test.tsx +142 -0
- package/src/shared/components/common/brand-header.tsx +93 -0
- package/src/shared/components/cron-config.tsx +1 -1
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +1 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +1 -1
- package/src/shared/components/search-config.tsx +3 -3
- package/src/shared/lib/api/README.md +3 -0
- package/src/shared/lib/api/index.ts +13 -11
- package/src/shared/lib/api/ncp-session.test.ts +17 -18
- package/src/shared/lib/api/ncp-session.types.ts +92 -0
- package/src/shared/lib/api/raw-client.utils.ts +3 -126
- package/src/shared/lib/api/services/agents.service.ts +18 -0
- package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
- package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
- package/src/shared/lib/api/services/config.service.ts +171 -0
- package/src/shared/lib/api/services/marketplace.service.ts +66 -0
- package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
- package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
- package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
- package/src/shared/lib/api/services/remote.service.ts +50 -0
- package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
- package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
- package/src/shared/lib/api/services/server-path.service.ts +16 -0
- package/src/shared/lib/api/types.ts +9 -74
- package/src/shared/lib/i18n/{chat.ts → chat-labels.utils.ts} +13 -1
- package/src/shared/lib/i18n/desktop-update-labels.utils.ts +65 -0
- package/src/shared/lib/i18n/index.ts +4 -5
- package/src/shared/lib/i18n/runtime/i18n-language-owner.ts +5 -5
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/local-transport.service.ts +24 -4
- package/src/shared/lib/transport/remote-transport.service.ts +2 -2
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
- package/src/shared/lib/transport/transport.types.ts +8 -2
- package/src/shared/lib/ui-document-title/index.ts +1 -1
- package/tsconfig.json +1 -0
- package/dist/assets/api-BurjmW4A.js +0 -15
- package/dist/assets/app-manager-provider-DhxUmyTv.js +0 -1
- package/dist/assets/app-navigation.config-Bpd16Pem.js +0 -1
- package/dist/assets/chat-page-Cc7n80lW.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-24FLdHl7.js +0 -3
- package/dist/assets/desktop-update-config-fMLlSStv.js +0 -1
- package/dist/assets/doc-browser-COj7x090.js +0 -1
- package/dist/assets/doc-browser-fyn7eDTp.js +0 -1
- package/dist/assets/i18n-CM4y8Mw9.js +0 -1
- package/dist/assets/index-CtVSzMPM.js +0 -2
- package/dist/assets/index-N3hjuljD.css +0 -1
- package/dist/assets/loader-circle-R23uEPkM.js +0 -1
- package/dist/assets/marketplace-page-mF-M5mku.js +0 -1
- package/dist/assets/mcp-marketplace-page-BArKWcRZ.js +0 -40
- package/dist/assets/mcp-marketplace-page-DBUcIIHJ.js +0 -1
- package/dist/assets/message-square-Dm34zD6k.js +0 -1
- package/dist/assets/play-ul4L6MWm.js +0 -1
- package/dist/assets/plus-D14303DH.js +0 -1
- package/dist/assets/remote-B4ELSd3u.js +0 -1
- package/dist/assets/runtime-config-page-N4FP6H0M.js +0 -1
- package/dist/assets/search-config-B62TY-z2.js +0 -1
- package/dist/assets/skeleton-BCPi52jT.js +0 -1
- package/dist/assets/theme-provider-WTWq_jYq.js +0 -1
- package/dist/assets/use-config-CyvhbRhf.js +0 -1
- package/dist/assets/x-tYcSDsrY.js +0 -1
- package/src/shared/lib/api/agents.ts +0 -34
- package/src/shared/lib/api/channel-auth.ts +0 -35
- package/src/shared/lib/api/config.ts +0 -362
- package/src/shared/lib/api/marketplace.ts +0 -156
- package/src/shared/lib/api/mcp-marketplace.ts +0 -138
- package/src/shared/lib/api/ncp-attachments.ts +0 -41
- package/src/shared/lib/api/ncp-session.ts +0 -78
- package/src/shared/lib/api/remote.ts +0 -86
- package/src/shared/lib/api/runtime-control.ts +0 -34
- package/src/shared/lib/api/server-path.ts +0 -46
- /package/dist/assets/{config-hints-CPNzbMEp.js → config-hints-MogHYQ8G.js} +0 -0
|
@@ -8,12 +8,16 @@ const captures = vi.hoisted(() => ({
|
|
|
8
8
|
language: "en",
|
|
9
9
|
}));
|
|
10
10
|
|
|
11
|
-
vi.mock("@nextclaw/agent-chat-ui", () =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
return {
|
|
14
|
+
...(actual as object),
|
|
15
|
+
ChatMessageList: (props: { messages: unknown[]; texts?: Record<string, unknown> }) => {
|
|
16
|
+
captures.renders.push(props);
|
|
17
|
+
return <div data-testid="chat-message-list" />;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
17
21
|
|
|
18
22
|
vi.mock("@/app/components/i18n-provider", () => ({
|
|
19
23
|
useI18n: () => ({ language: captures.language }),
|
|
@@ -211,3 +215,57 @@ it("passes localized attachment card texts to the shared chat UI", () => {
|
|
|
211
215
|
},
|
|
212
216
|
});
|
|
213
217
|
});
|
|
218
|
+
|
|
219
|
+
it("renders context compaction as an in-flow divider instead of a chat message", () => {
|
|
220
|
+
const beforeMessage = {
|
|
221
|
+
id: "message-before",
|
|
222
|
+
sessionId: "session-1",
|
|
223
|
+
role: "user",
|
|
224
|
+
status: "final",
|
|
225
|
+
timestamp: "2026-05-05T11:59:00.000Z",
|
|
226
|
+
parts: [{ type: "text", text: "before" }],
|
|
227
|
+
} satisfies NcpMessage;
|
|
228
|
+
const afterMessage = {
|
|
229
|
+
id: "message-after",
|
|
230
|
+
sessionId: "session-1",
|
|
231
|
+
role: "assistant",
|
|
232
|
+
status: "final",
|
|
233
|
+
timestamp: "2026-05-05T12:01:00.000Z",
|
|
234
|
+
parts: [{ type: "text", text: "after" }],
|
|
235
|
+
} satisfies NcpMessage;
|
|
236
|
+
const compactionMessage = {
|
|
237
|
+
id: "ctx-message",
|
|
238
|
+
sessionId: "session-1",
|
|
239
|
+
role: "service",
|
|
240
|
+
status: "final",
|
|
241
|
+
timestamp: "2026-05-05T12:00:00.000Z",
|
|
242
|
+
metadata: {
|
|
243
|
+
nextclaw_timeline_kind: "context_compaction",
|
|
244
|
+
checkpoint: {
|
|
245
|
+
id: "ctx-1",
|
|
246
|
+
status: "compressed",
|
|
247
|
+
summary: "Compressed Earlier Context",
|
|
248
|
+
coveredMessageCount: 8,
|
|
249
|
+
coveredSessionMessageCount: 8,
|
|
250
|
+
originalEstimatedTokens: 76000,
|
|
251
|
+
projectedEstimatedTokens: 51000,
|
|
252
|
+
createdAt: "2026-05-05T11:59:50.000Z",
|
|
253
|
+
updatedAt: "2026-05-05T12:00:00.000Z",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
parts: [{ type: "text", text: "较早上下文已自动压缩" }],
|
|
257
|
+
} satisfies NcpMessage;
|
|
258
|
+
|
|
259
|
+
const { getByText } = render(
|
|
260
|
+
<ChatMessageListContainer
|
|
261
|
+
messages={[beforeMessage, compactionMessage, afterMessage]}
|
|
262
|
+
isSending={false}
|
|
263
|
+
/>,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(getByText("chatContextCompactionCompressed")).toBeTruthy();
|
|
267
|
+
const renderedGroups = captures.renders.map((rendered) => rendered.messages);
|
|
268
|
+
expect(renderedGroups).toHaveLength(2);
|
|
269
|
+
expect(renderedGroups[0]).toMatchObject([{ id: "message-before" }]);
|
|
270
|
+
expect(renderedGroups[1]).toMatchObject([{ id: "message-after" }]);
|
|
271
|
+
});
|
|
@@ -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.
|
|
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
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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)
|
|
@@ -6,8 +6,7 @@ import { sessionMatchesQuery } from '@/features/chat/utils/chat-session-display.
|
|
|
6
6
|
import { adaptNcpSessionSummaries } from '@/features/chat/utils/ncp-session-adapter.utils';
|
|
7
7
|
import { useChatSessionTypeState } from '@/features/chat/hooks/use-chat-session-type-state';
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
resolveRecentSessionPreferredModel,
|
|
9
|
+
resolveRecentSessionPreferredValue,
|
|
11
10
|
useSyncSelectedModel,
|
|
12
11
|
useSyncSelectedThinking
|
|
13
12
|
} from '@/features/chat/utils/chat-session-preference-governance.utils';
|
|
@@ -83,19 +82,21 @@ function useRecentSessionPreferences(params: {
|
|
|
83
82
|
const { sessions, sessionKey, sessionType } = params;
|
|
84
83
|
const recentSessionPreferredModel = useMemo(
|
|
85
84
|
() =>
|
|
86
|
-
|
|
85
|
+
resolveRecentSessionPreferredValue<string>({
|
|
87
86
|
sessions,
|
|
88
87
|
selectedSessionKey: sessionKey,
|
|
89
|
-
sessionType
|
|
88
|
+
sessionType,
|
|
89
|
+
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
90
90
|
}),
|
|
91
91
|
[sessionKey, sessionType, sessions]
|
|
92
92
|
);
|
|
93
93
|
const recentSessionPreferredThinking = useMemo(
|
|
94
94
|
() =>
|
|
95
|
-
|
|
95
|
+
resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
96
96
|
sessions,
|
|
97
97
|
selectedSessionKey: sessionKey,
|
|
98
|
-
sessionType
|
|
98
|
+
sessionType,
|
|
99
|
+
readPreference: (session) => session.preferredThinking ?? undefined
|
|
99
100
|
}),
|
|
100
101
|
[sessionKey, sessionType, sessions]
|
|
101
102
|
);
|
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
systemStatus.phase === "ready" &&
|
|
114
130
|
isNcpAgentStartupUnavailableErrorMessage(currentAgentError)
|
|
115
|
-
? `${sessionId}:${
|
|
131
|
+
? `${sessionId}:${systemStatus.lastReadyAt ?? 0}`
|
|
116
132
|
: null;
|
|
117
133
|
useSyncReadyRetryVersion(readyRetrySignature, () => {
|
|
118
134
|
setHydrationRetryVersion((current) => current + 1);
|
|
119
135
|
});
|
|
120
|
-
return
|
|
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 = {
|