@nextclaw/ui 0.12.18 → 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.
- package/CHANGELOG.md +33 -0
- package/dist/assets/api-C412zuay.js +15 -0
- package/dist/assets/app-manager-provider-Cm-KiZZG.js +1 -0
- package/dist/assets/app-navigation.config-BORqHkbN.js +1 -0
- package/dist/assets/{book-open-CUd69I2f.js → book-open-DgLqYpNY.js} +1 -1
- package/dist/assets/{channels-list-page-5wQy-UW7.js → channels-list-page-sISO_4Yj.js} +2 -2
- package/dist/assets/{chat-C7Ywus_K.js → chat-ChCu7LQD.js} +13 -12
- package/dist/assets/chat-page-BCaNZJGT.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-THqEFwu9.js → chunk-JZWAC4HX-DvbcIVPf.js} +1 -1
- package/dist/assets/{config-split-page-UJSTBsEU.js → config-split-page-BMRGuCJQ.js} +1 -1
- package/dist/assets/{createLucideIcon-DVAlgDOi.js → createLucideIcon-BZkY6emz.js} +1 -1
- package/dist/assets/desktop-update-config-BfJ5iSeY.js +1 -0
- package/dist/assets/{dialog-t7OAmObC.js → dialog-B-CXiFPZ.js} +1 -1
- package/dist/assets/{dist-DWPNydLC.js → dist-DYVfg3q5.js} +1 -1
- package/dist/assets/{doc-browser-DznuT-CU.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-zXaTjrpA.js → doc-browser-context-DfLHAWbG.js} +1 -1
- package/dist/assets/{es2015-BRVsmfFO.js → es2015-BXroVnPi.js} +1 -1
- package/dist/assets/{external-link-D1Xqff6i.js → external-link-Sw3ah_JD.js} +1 -1
- package/dist/assets/{folder-B_fuaX3x.js → folder-D7-VTnkz.js} +1 -1
- package/dist/assets/{hash-BRvv_UUq.js → hash-zajSTDXZ.js} +1 -1
- package/dist/assets/i18n-C5Mibli1.js +1 -0
- package/dist/assets/index-CUmk8xFK.css +1 -0
- package/dist/assets/index-CqPDhosM.js +2 -0
- package/dist/assets/{key-round-DFVNXZcD.js → key-round-CnI1mc9F.js} +1 -1
- package/dist/assets/loader-circle-B5i8oMMY.js +1 -0
- package/dist/assets/{logo-badge-CRVKkIl9.js → logo-badge-BQgKnVtz.js} +1 -1
- package/dist/assets/{logos-BVCi_7_I.js → logos-CqVm0q0W.js} +1 -1
- package/dist/assets/marketplace-page-C8uaWkfd.js +1 -0
- package/dist/assets/{marketplace-page-apq5LpYx.js → marketplace-page-C9oZ01rM.js} +2 -2
- package/dist/assets/mcp-marketplace-page-DuEixgSs.js +40 -0
- package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +1 -0
- package/dist/assets/message-square-D6Z4NwpG.js +1 -0
- package/dist/assets/{model-config-cth12uRn.js → model-config-mfhqEZBG.js} +1 -1
- package/dist/assets/{notice-card-CXY09tsa.js → notice-card-CozHB03G.js} +1 -1
- package/dist/assets/play-D8WJLnJe.js +1 -0
- package/dist/assets/plus-Di0KAkiO.js +1 -0
- package/dist/assets/{popover-CbQxrchk.js → popover-CPUPma-w.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CZEB2m98.js → provider-scoped-model-input-CL9sti2I.js} +1 -1
- package/dist/assets/{providers-list-Dsj2BYPm.js → providers-list-HPmL2akJ.js} +1 -1
- package/dist/assets/{refresh-ccw-C-ytTHiq.js → refresh-ccw-Bii4w8aB.js} +1 -1
- package/dist/assets/refresh-cw-BxojR62w.js +1 -0
- package/dist/assets/remote-oDlAdgVA.js +1 -0
- package/dist/assets/{rotate-cw-ClSrRUa0.js → rotate-cw-1Xqa7LZ8.js} +1 -1
- package/dist/assets/runtime-config-page-BCshTAAE.js +1 -0
- package/dist/assets/{save-KxhpE3Zr.js → save--BVI5wZX.js} +1 -1
- package/dist/assets/search-config-Bcnk9VlL.js +1 -0
- package/dist/assets/{search-Bz3Q64sr.js → search-vChioOoe.js} +1 -1
- package/dist/assets/{secrets-config-BOL024Fj.js → secrets-config-Dde-5Y1w.js} +2 -2
- package/dist/assets/{select-tRTLG4FK.js → select-BELPuXLW.js} +1 -1
- package/dist/assets/{sessions-config-page-Ck6nbIFq.js → sessions-config-page-CG49_0Z6.js} +2 -2
- package/dist/assets/{setting-row-DMDgBCC7.js → setting-row-D5DtT6Ny.js} +1 -1
- package/dist/assets/{settings-Cto6z-Ij.js → settings-CiRChctQ.js} +1 -1
- package/dist/assets/skeleton-CFQRIUzt.js +1 -0
- package/dist/assets/{sparkles-xZ74eW0P.js → sparkles-D1ZKWdm4.js} +1 -1
- package/dist/assets/{status-dot-Bobpfutv.js → status-dot-Dv_hiUVa.js} +1 -1
- package/dist/assets/{tabs-custom-C3Mf-NLb.js → tabs-custom-CsACkVji.js} +1 -1
- package/dist/assets/{tag-chip-BZ14i5b1.js → tag-chip-D9BWWgYg.js} +1 -1
- package/dist/assets/theme-provider-DeBrTglS.js +1 -0
- package/dist/assets/{tooltip-7lsLGcL9.js → tooltip-CI0rpNee.js} +1 -1
- package/dist/assets/{trash-2-WFSNa5oj.js → trash-2-rY9ZteZX.js} +1 -1
- package/dist/assets/use-config-CrWZ_TSF.js +1 -0
- package/dist/assets/{use-confirm-dialog-Df1SozKw.js → use-confirm-dialog-hbynwWf2.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-BogRuzgz.js → use-infinite-scroll-loader-Cw5qQr3-.js} +1 -1
- package/dist/assets/{use-viewport-layout-CWlW5b-T.js → use-viewport-layout-CWHVDC6z.js} +1 -1
- package/dist/assets/x-DpTzXQcX.js +1 -0
- package/dist/index.html +40 -39
- package/package.json +7 -6
- package/src/app/index.tsx +7 -1
- 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-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.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/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 +7 -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 +2 -0
- package/src/shared/lib/api/ncp-attachments.ts +2 -2
- package/src/shared/lib/api/ncp-session.types.ts +92 -0
- package/src/shared/lib/api/runtime-update.service.ts +50 -0
- package/src/shared/lib/api/types.ts +14 -84
- 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 +5 -6
- package/src/shared/lib/i18n/runtime/i18n-language-owner.ts +5 -5
- package/src/shared/lib/transport/remote-transport.service.ts +1 -1
- package/src/shared/lib/ui-document-title/index.ts +1 -1
- package/tsconfig.json +1 -0
- package/dist/assets/api-BIg--UMJ.js +0 -15
- package/dist/assets/app-manager-provider-BfKiVYea.js +0 -1
- package/dist/assets/app-navigation.config-xIjCAn-R.js +0 -1
- package/dist/assets/chat-page-DX8OMxQ_.js +0 -1
- package/dist/assets/desktop-update-config-DdVgauFR.js +0 -1
- package/dist/assets/doc-browser-BIggpN8Z.js +0 -1
- package/dist/assets/doc-browser-t96ibd-b.js +0 -1
- package/dist/assets/i18n-CF_jgT_-.js +0 -1
- package/dist/assets/index-CLxN8vXZ.js +0 -2
- package/dist/assets/index-N3hjuljD.css +0 -1
- package/dist/assets/loader-circle-DEz3bHGb.js +0 -1
- package/dist/assets/marketplace-page-DVmk8dZk.js +0 -1
- package/dist/assets/mcp-marketplace-page-CskrJuKU.js +0 -1
- package/dist/assets/mcp-marketplace-page-DiqTAdRJ.js +0 -40
- package/dist/assets/message-square-CLhDWybk.js +0 -1
- package/dist/assets/play-CnnPm8ca.js +0 -1
- package/dist/assets/plus-CdYMdiws.js +0 -1
- package/dist/assets/remote-DMMC2PSo.js +0 -1
- package/dist/assets/runtime-config-page-y8HmA9qr.js +0 -1
- package/dist/assets/search-config-C_xRBv_i.js +0 -1
- package/dist/assets/skeleton-BwfJfVK3.js +0 -1
- package/dist/assets/theme-provider-D6Cgm6i-.js +0 -1
- package/dist/assets/use-config-CXu7dFzw.js +0 -1
- package/dist/assets/x-BvS2y4e_.js +0 -1
- /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.
|
|
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)
|
|
@@ -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 = {
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
isRuntimeBlocked
|
|
235
236
|
? null
|
|
236
|
-
:
|
|
237
|
+
: systemStatus.phase === "ready"
|
|
237
238
|
? filteredLastSendError
|
|
238
|
-
:
|
|
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 {
|
|
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) => ({
|