@nextclaw/ui 0.20.0 → 0.20.2
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 +36 -0
- package/dist/assets/{api-C2iYhdTe.js → api-DvpQWjTS.js} +3 -3
- package/dist/assets/{appearance-settings-page-BwQDQr0n.js → appearance-settings-page-U0m0ktu3.js} +1 -1
- package/dist/assets/{book-open-C0IlT9zs.js → book-open-DRofSTWr.js} +1 -1
- package/dist/assets/channels-list-page-BURQarD0.js +8 -0
- package/dist/assets/{config-split-page-VDTf3zZb.js → config-split-page-BrAQYTIl.js} +1 -1
- package/dist/assets/{confirm-dialog-BI_Vt2s_.js → confirm-dialog-DDJomLnO.js} +1 -1
- package/dist/assets/{createLucideIcon-Bydi9Gc5.js → createLucideIcon-DzkNHr5Z.js} +1 -1
- package/dist/assets/{desktop-update-config-DVoD6NxG.js → desktop-update-config-BzZFhMv9.js} +1 -1
- package/dist/assets/{dist-CmHtkFbs.js → dist-DXbLo2b1.js} +1 -1
- package/dist/assets/{dist-A5btQciA.js → dist-NskPB_yW.js} +1 -1
- package/dist/assets/doc-browser-DB22tQKU.js +1 -0
- package/dist/assets/{doc-browser-context-Bxw0ora-.js → doc-browser-context-CnKV4gJy.js} +1 -1
- package/dist/assets/doc-browser-sJjw9fl5.js +1 -0
- package/dist/assets/{extensions-CG4voDCh.js → extensions-DvlFMpAs.js} +1 -1
- package/dist/assets/{form-actions-lv47uGIB.js → form-actions-UnPEZBkx.js} +1 -1
- package/dist/assets/index-Dc_ZMkmo.css +1 -0
- package/dist/assets/index-N1HnesBp.js +109 -0
- package/dist/assets/loader-circle-BlPXxgDB.js +1 -0
- package/dist/assets/mcp-marketplace-page-BIgFTbMq.js +9 -0
- package/dist/assets/mcp-marketplace-page-CZN4ovdK.js +1 -0
- package/dist/assets/model-config-page-CnSm7kfa.js +1 -0
- package/dist/assets/{navigation-link-D0agTj5t.js → navigation-link-NjUdG6Vp.js} +1 -1
- package/dist/assets/plus-DVXol9Q5.js +1 -0
- package/dist/assets/privacy-settings-page-DVn2HU7W.js +1 -0
- package/dist/assets/providers-config-page-CzHJS3eg.js +1 -0
- package/dist/assets/react-DCVbGroo.js +8 -0
- package/dist/assets/{refresh-cw-DjgY2Eju.js → refresh-cw-CkeninE0.js} +1 -1
- package/dist/assets/remote-B2lsJF1c.js +1 -0
- package/dist/assets/{rotate-cw-Dot4PVh0.js → rotate-cw-DP33PuXU.js} +1 -1
- package/dist/assets/runtime-config-page-D69buyGp.js +1 -0
- package/dist/assets/{save-B-jdTe_c.js → save-Cw2Xz-pB.js} +1 -1
- package/dist/assets/{search-CEp2AIiV.js → search-C2CFAVvd.js} +1 -1
- package/dist/assets/{search-config-page-Dp5guS5O.js → search-config-page-dqNT0Swf.js} +1 -1
- package/dist/assets/{secrets-config-page-C7nRMjiv.js → secrets-config-page-De6J3wVx.js} +2 -2
- package/dist/assets/security-config-Dcu9C4OL.js +1 -0
- package/dist/assets/{select-BNA3cma8.js → select-DSuMhwVP.js} +1 -1
- package/dist/assets/skeleton-QoWydNLp.js +1 -0
- package/dist/assets/{tag-chip-DmCNi4-9.js → tag-chip-DFBLgsGf.js} +1 -1
- package/dist/assets/x-BXvAzpjb.js +1 -0
- package/dist/index.html +21 -21
- package/package.json +8 -8
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +4 -3
- package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +11 -4
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +17 -17
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +17 -5
- package/src/features/chat/components/layout/chat-sidebar.tsx +28 -21
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +41 -1
- package/src/features/chat/features/conversation/hooks/use-session-pending-input-actions.ts +22 -4
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +33 -4
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +186 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +9 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +71 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-trigger-details.utils.test.ts +147 -0
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +36 -0
- package/src/features/chat/features/message/utils/chat-message-trigger-details.utils.ts +118 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +50 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +90 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +24 -3
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +35 -19
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +26 -2
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +55 -27
- package/src/features/chat/features/session/components/chat-sidebar-context-card.tsx +60 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +95 -54
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +9 -8
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +141 -95
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +11 -2
- package/src/features/chat/features/session/hooks/use-chat-sidebar-context-counts.ts +40 -0
- package/src/features/chat/features/session/utils/ncp-session-context-metadata.utils.ts +5 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +16 -9
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +5 -5
- package/src/features/chat/features/workspace/components/__tests__/workspace-markdown-outline.test.tsx +128 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +20 -2
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -40
- package/src/features/chat/features/workspace/components/workspace-markdown-outline.tsx +156 -0
- package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +70 -0
- package/src/features/chat/managers/chat-completion-notification.manager.ts +23 -6
- package/src/features/settings/pages/__tests__/privacy-settings-page.test.tsx +14 -5
- package/src/features/settings/pages/privacy-settings-page.tsx +38 -2
- package/src/shared/components/ui/actions/icon-action-button.tsx +1 -8
- package/src/shared/components/ui/tooltip.tsx +16 -1
- package/src/shared/hooks/use-config.ts +11 -0
- package/src/shared/lib/api/types.ts +8 -0
- package/src/shared/lib/api/utils/config.utils.ts +5 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +28 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +17 -9
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +28 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +17 -9
- package/dist/assets/channels-list-page-B0z4bh8e.js +0 -8
- package/dist/assets/doc-browser-Cm_tMZ_C.js +0 -1
- package/dist/assets/doc-browser-Dto54tCf.js +0 -1
- package/dist/assets/index-BTSwNDVi.js +0 -109
- package/dist/assets/index-QrdUWHfm.css +0 -1
- package/dist/assets/loader-circle-CkaqeEne.js +0 -1
- package/dist/assets/mcp-marketplace-page-BSk0qUOX.js +0 -9
- package/dist/assets/mcp-marketplace-page-BgZGc-S_.js +0 -1
- package/dist/assets/model-config-page-c8RnUGoH.js +0 -1
- package/dist/assets/plus-BiMjkv3R.js +0 -1
- package/dist/assets/privacy-settings-page-CPkyTvEE.js +0 -1
- package/dist/assets/providers-config-page-CGUEb5Dx.js +0 -1
- package/dist/assets/react-BMYFkaGi.js +0 -8
- package/dist/assets/remote-1LxkpRED.js +0 -1
- package/dist/assets/runtime-config-page-C6nzqTHg.js +0 -1
- package/dist/assets/security-config-DvHUDxSg.js +0 -1
- package/dist/assets/skeleton-CbroEO8Q.js +0 -1
- package/dist/assets/x-BTo5-A8h.js +0 -1
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
useCreateProject,
|
|
22
22
|
useProjects,
|
|
23
23
|
} from "@/shared/hooks/use-projects";
|
|
24
|
-
import type {
|
|
24
|
+
import type {
|
|
25
|
+
ProjectAddExistingRequest,
|
|
26
|
+
ProjectCreateRequest,
|
|
27
|
+
} from "@/shared/lib/api";
|
|
25
28
|
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
26
29
|
import { cn } from "@/shared/lib/utils";
|
|
27
30
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from "@/shared/lib/i18n";
|
|
@@ -50,6 +53,7 @@ import { useChatNewSessionTypePreference } from "@/features/chat/features/sessio
|
|
|
50
53
|
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
51
54
|
import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
|
|
52
55
|
import { ChatProjectAddDialog } from "@/features/chat/features/project/components/chat-project-add-dialog";
|
|
56
|
+
import { useChatSidebarContextCounts } from "@/features/chat/features/session/hooks/use-chat-sidebar-context-counts";
|
|
53
57
|
|
|
54
58
|
type ChatSidebarVariant = "desktop" | "mobile";
|
|
55
59
|
|
|
@@ -116,7 +120,9 @@ export function ChatSidebar({
|
|
|
116
120
|
const projectsQuery = useProjects();
|
|
117
121
|
const projectCreateMutation = useCreateProject();
|
|
118
122
|
const projectAddExistingMutation = useAddExistingProject();
|
|
119
|
-
const { isLoading, items } = useNcpSessionListView();
|
|
123
|
+
const { allItems, isLoading, items } = useNcpSessionListView();
|
|
124
|
+
const { cronJobCountByProjectRoot, cronJobCountBySessionKey } =
|
|
125
|
+
useChatSidebarContextCounts(allItems);
|
|
120
126
|
const { language, setLanguage } = useI18n();
|
|
121
127
|
const { theme, setTheme } = useTheme();
|
|
122
128
|
const currentThemeLabel = t(
|
|
@@ -160,21 +166,27 @@ export function ChatSidebar({
|
|
|
160
166
|
[items],
|
|
161
167
|
);
|
|
162
168
|
const childSessionsByParentKey = useMemo(
|
|
163
|
-
() => groupChildSessionsByParentKey(
|
|
164
|
-
[
|
|
169
|
+
() => groupChildSessionsByParentKey(allItems),
|
|
170
|
+
[allItems],
|
|
165
171
|
);
|
|
166
172
|
const groups = useMemo(
|
|
167
173
|
() => groupSessionsByDate(sortedItems, pinnedSessionKeys),
|
|
168
174
|
[pinnedSessionKeys, sortedItems],
|
|
169
175
|
);
|
|
170
176
|
const projectGroups = useMemo(
|
|
171
|
-
() =>
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
() =>
|
|
178
|
+
groupSessionsByProject(
|
|
179
|
+
sortedItems,
|
|
180
|
+
pinnedSessionKeys,
|
|
181
|
+
pinnedProjectRoots,
|
|
182
|
+
projectsQuery.data?.projects ?? [],
|
|
183
|
+
),
|
|
184
|
+
[
|
|
174
185
|
pinnedProjectRoots,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
pinnedSessionKeys,
|
|
187
|
+
projectsQuery.data?.projects,
|
|
188
|
+
sortedItems,
|
|
189
|
+
],
|
|
178
190
|
);
|
|
179
191
|
const sessionTypeOptions = useMemo(
|
|
180
192
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
@@ -220,6 +232,7 @@ export function ChatSidebar({
|
|
|
220
232
|
optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
|
|
221
233
|
agentsById={agentsById}
|
|
222
234
|
childSessionsByParentKey={childSessionsByParentKey}
|
|
235
|
+
cronJobCount={cronJobCountBySessionKey.get(item.session.key) ?? 0}
|
|
223
236
|
editingSessionKey={editingSessionKey}
|
|
224
237
|
draftLabel={draftLabel}
|
|
225
238
|
savingSessionKey={savingSessionKey}
|
|
@@ -227,12 +240,6 @@ export function ChatSidebar({
|
|
|
227
240
|
isPinned={pinnedSessionKeys.has(item.session.key)}
|
|
228
241
|
sessionTitle={getSessionTitle}
|
|
229
242
|
onSelectSession={presenter.chatSessionListManager.selectSession}
|
|
230
|
-
onOpenChildSessions={(parentSessionKey, activeChildSessionKey) =>
|
|
231
|
-
presenter.chatThreadManager.openChildSessionPanel({
|
|
232
|
-
parentSessionKey,
|
|
233
|
-
activeChildSessionKey,
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
243
|
onStartEditingSessionLabel={startEditingSessionLabel}
|
|
237
244
|
onDraftLabelChange={setDraftLabel}
|
|
238
245
|
onSaveSessionLabel={saveSessionLabel}
|
|
@@ -256,7 +263,9 @@ export function ChatSidebar({
|
|
|
256
263
|
projectAddExistingMutation.reset();
|
|
257
264
|
setIsProjectAddOpen(true);
|
|
258
265
|
};
|
|
259
|
-
const createProjectFromSidebar = async (
|
|
266
|
+
const createProjectFromSidebar = async (
|
|
267
|
+
input: ProjectCreateRequest,
|
|
268
|
+
): Promise<void> => {
|
|
260
269
|
await projectCreateMutation.mutateAsync(input);
|
|
261
270
|
setIsProjectAddOpen(false);
|
|
262
271
|
};
|
|
@@ -274,10 +283,7 @@ export function ChatSidebar({
|
|
|
274
283
|
isMobileVariant
|
|
275
284
|
? "flex-1 overflow-hidden"
|
|
276
285
|
: shouldCollapse
|
|
277
|
-
? cn(
|
|
278
|
-
SIDEBAR_RAIL_WIDTH_CLASS,
|
|
279
|
-
"shrink-0",
|
|
280
|
-
)
|
|
286
|
+
? cn(SIDEBAR_RAIL_WIDTH_CLASS, "shrink-0")
|
|
281
287
|
: "w-[280px] shrink-0",
|
|
282
288
|
)}
|
|
283
289
|
data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
|
|
@@ -339,6 +345,7 @@ export function ChatSidebar({
|
|
|
339
345
|
onAddProject={openProjectAdd}
|
|
340
346
|
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
341
347
|
projectGroups={projectGroups}
|
|
348
|
+
projectCronJobCountByRoot={cronJobCountByProjectRoot}
|
|
342
349
|
renderSessionItem={renderSessionItem}
|
|
343
350
|
sessionTypeOptions={sessionTypeOptions}
|
|
344
351
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import type
|
|
3
|
+
import { NextClawClientError, type UiNcpSessionQueuedInputView } from '@nextclaw/client-sdk';
|
|
4
4
|
import type { NcpAgentSendEnvelope, NcpMessage, NcpRunHandle } from '@nextclaw/ncp';
|
|
5
5
|
|
|
6
6
|
import { useSessionConversationController } from '@/features/chat/features/conversation/hooks/use-session-conversation-controller';
|
|
@@ -448,3 +448,43 @@ describe('useSessionConversationController backend run queue', () => {
|
|
|
448
448
|
expect(params.onSessionMaterialized).not.toHaveBeenCalled();
|
|
449
449
|
});
|
|
450
450
|
});
|
|
451
|
+
|
|
452
|
+
describe('useSessionConversationController stale queue reconciliation', () => {
|
|
453
|
+
it('reconciles repeated stale queued actions without exposing their not-found errors', async () => {
|
|
454
|
+
const queuedInput = createQueuedInput();
|
|
455
|
+
const params = createControllerParams({ isRunning: true, queuedInputs: [queuedInput] });
|
|
456
|
+
params.runQueue.steerQueuedInput.mockRejectedValue(new NextClawClientError({
|
|
457
|
+
code: 'NOT_FOUND',
|
|
458
|
+
message: `queued input not found in session session-1: ${queuedInput.id}`,
|
|
459
|
+
status: 404,
|
|
460
|
+
}));
|
|
461
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
462
|
+
|
|
463
|
+
act(() => result.current.steerQueuedInput(queuedInput.id));
|
|
464
|
+
await waitFor(() => expect(params.runQueue.refreshPendingInputs).toHaveBeenCalledTimes(1));
|
|
465
|
+
|
|
466
|
+
act(() => result.current.steerQueuedInput(queuedInput.id));
|
|
467
|
+
await waitFor(() => expect(params.runQueue.refreshPendingInputs).toHaveBeenCalledTimes(2));
|
|
468
|
+
|
|
469
|
+
expect(params.setSendError).toHaveBeenLastCalledWith(null);
|
|
470
|
+
expect(params.setSendError).not.toHaveBeenCalledWith(expect.stringContaining('queued input not found'));
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('keeps a real steering failure visible', async () => {
|
|
474
|
+
const queuedInput = createQueuedInput();
|
|
475
|
+
const params = createControllerParams({ isRunning: true, queuedInputs: [queuedInput] });
|
|
476
|
+
params.runQueue.steerQueuedInput.mockRejectedValue(new NextClawClientError({
|
|
477
|
+
code: 'STEER_UNAVAILABLE',
|
|
478
|
+
message: 'The active runtime cannot accept this input at the next safe step.',
|
|
479
|
+
status: 409,
|
|
480
|
+
}));
|
|
481
|
+
const { result } = renderHook(() => useSessionConversationController(params));
|
|
482
|
+
|
|
483
|
+
act(() => result.current.steerQueuedInput(queuedInput.id));
|
|
484
|
+
|
|
485
|
+
await waitFor(() => expect(params.setSendError).toHaveBeenCalledWith(
|
|
486
|
+
'The active runtime cannot accept this input at the next safe step.',
|
|
487
|
+
));
|
|
488
|
+
expect(params.runQueue.refreshPendingInputs).not.toHaveBeenCalled();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
NextClawClientError,
|
|
4
|
+
type UiNcpSessionPendingInputView,
|
|
5
|
+
type UiNcpSessionQueuedInputView,
|
|
5
6
|
} from '@nextclaw/client-sdk';
|
|
6
7
|
import type { NcpAgentSendEnvelope } from '@nextclaw/ncp';
|
|
7
8
|
|
|
@@ -20,10 +21,17 @@ type ComposerDraftSnapshot = Pick<
|
|
|
20
21
|
|
|
21
22
|
type SessionRunQueue = {
|
|
22
23
|
readonly inputs: readonly UiNcpSessionQueuedInputView[];
|
|
24
|
+
readonly refreshPendingInputs: () => Promise<readonly UiNcpSessionPendingInputView[]>;
|
|
23
25
|
readonly removeQueuedInput: (id: string) => Promise<UiNcpSessionQueuedInputView | null>;
|
|
24
26
|
readonly steerQueuedInput: (id: string) => Promise<UiNcpSessionPendingInputView | null>;
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
function isStaleQueuedInputError(error: unknown): boolean {
|
|
30
|
+
return error instanceof NextClawClientError
|
|
31
|
+
&& error.status === 404
|
|
32
|
+
&& error.code === 'NOT_FOUND';
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export type SubmittingQueuedInput = {
|
|
28
36
|
readonly attachments: readonly SessionQueuedInputAttachmentPreview[];
|
|
29
37
|
readonly id: string;
|
|
@@ -127,7 +135,17 @@ export function useQueuedInputActions(params: {
|
|
|
127
135
|
const steerQueuedInput = useCallback((id: string) => {
|
|
128
136
|
void runQueue.steerQueuedInput(id).then((steered) => {
|
|
129
137
|
if (steered) setSendError(null);
|
|
130
|
-
}).catch((error) => {
|
|
138
|
+
}).catch(async (error) => {
|
|
139
|
+
if (isStaleQueuedInputError(error)) {
|
|
140
|
+
try {
|
|
141
|
+
await runQueue.refreshPendingInputs();
|
|
142
|
+
setSendError(null);
|
|
143
|
+
return;
|
|
144
|
+
} catch (refreshError) {
|
|
145
|
+
setSendError(refreshError instanceof Error ? refreshError.message : String(refreshError));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
131
149
|
setSendError(error instanceof Error ? error.message : String(error));
|
|
132
150
|
});
|
|
133
151
|
}, [runQueue, setSendError]);
|
|
@@ -20,9 +20,14 @@ import {
|
|
|
20
20
|
} from "@/features/chat/features/message/utils/chat-message.utils";
|
|
21
21
|
import { buildChatMessageProcessSummary } from "@/features/chat/features/message/utils/chat-message-process-summary.utils";
|
|
22
22
|
import { buildChatMessageExecutionPresentation } from "@/features/chat/features/message/utils/chat-message-execution-summary.utils";
|
|
23
|
+
import {
|
|
24
|
+
buildChatMessageTriggerDetails,
|
|
25
|
+
mergeChatMessageMoreActions,
|
|
26
|
+
} from "@/features/chat/features/message/utils/chat-message-trigger-details.utils";
|
|
23
27
|
import {
|
|
24
28
|
buildChatMessageAdapterTexts,
|
|
25
29
|
buildChatMessageExecutionLabels,
|
|
30
|
+
buildChatMessageTriggerLabels,
|
|
26
31
|
buildChatMessageTexts,
|
|
27
32
|
} from "@/features/chat/features/message/utils/chat-message-texts.utils";
|
|
28
33
|
import {
|
|
@@ -94,6 +99,7 @@ class ChatMessageViewModelAdapter {
|
|
|
94
99
|
language: Parameters<typeof formatDateTime>[1];
|
|
95
100
|
processSummaryLabel: string | null;
|
|
96
101
|
executionPresentationKey: string | null;
|
|
102
|
+
triggerDetailsKey: string | null;
|
|
97
103
|
viewModel: ChatMessageViewModel;
|
|
98
104
|
}
|
|
99
105
|
>();
|
|
@@ -101,13 +107,14 @@ class ChatMessageViewModelAdapter {
|
|
|
101
107
|
adapt = (params: {
|
|
102
108
|
continuationRunning: boolean;
|
|
103
109
|
executionLabels: ReturnType<typeof buildChatMessageExecutionLabels>;
|
|
110
|
+
triggerLabels: ReturnType<typeof buildChatMessageTriggerLabels>;
|
|
104
111
|
language: Parameters<typeof formatDateTime>[1];
|
|
105
112
|
processedLabel: string;
|
|
106
113
|
rawMessages: readonly NcpMessage[];
|
|
107
114
|
texts: ReturnType<typeof buildChatMessageAdapterTexts>;
|
|
108
115
|
}): ChatMessageViewModel[] => {
|
|
109
116
|
const {
|
|
110
|
-
continuationRunning, executionLabels, language, processedLabel, rawMessages, texts,
|
|
117
|
+
continuationRunning, executionLabels, triggerLabels, language, processedLabel, rawMessages, texts,
|
|
111
118
|
} = params;
|
|
112
119
|
return projectVisibleChatMessages(rawMessages, { continuationRunning }).map((message) => {
|
|
113
120
|
const processSummary = buildChatMessageProcessSummary({
|
|
@@ -127,12 +134,28 @@ class ChatMessageViewModelAdapter {
|
|
|
127
134
|
labels: executionLabels,
|
|
128
135
|
});
|
|
129
136
|
const executionPresentationKey = executionPresentation?.cacheKey ?? null;
|
|
137
|
+
const triggerDetails = buildChatMessageTriggerDetails({
|
|
138
|
+
message,
|
|
139
|
+
labels: triggerLabels,
|
|
140
|
+
});
|
|
141
|
+
const triggerDetailsKey = triggerDetails
|
|
142
|
+
? JSON.stringify({
|
|
143
|
+
runTrigger: message.metadata?.run_trigger,
|
|
144
|
+
runSpec: message.metadata?.run_spec,
|
|
145
|
+
aiExecution: message.metadata?.ai_execution,
|
|
146
|
+
})
|
|
147
|
+
: null;
|
|
148
|
+
const moreActions = mergeChatMessageMoreActions(
|
|
149
|
+
executionPresentation?.moreActions,
|
|
150
|
+
triggerDetails,
|
|
151
|
+
);
|
|
130
152
|
const cached = this.cache.get(message);
|
|
131
153
|
if (
|
|
132
154
|
cached &&
|
|
133
155
|
cached.language === language &&
|
|
134
156
|
cached.processSummaryLabel === processSummaryLabel &&
|
|
135
|
-
cached.executionPresentationKey === executionPresentationKey
|
|
157
|
+
cached.executionPresentationKey === executionPresentationKey &&
|
|
158
|
+
cached.triggerDetailsKey === triggerDetailsKey
|
|
136
159
|
) {
|
|
137
160
|
return cached.viewModel;
|
|
138
161
|
}
|
|
@@ -147,7 +170,7 @@ class ChatMessageViewModelAdapter {
|
|
|
147
170
|
inlineTokens: readInlineTokensFromMetadata(message.metadata),
|
|
148
171
|
processSummary,
|
|
149
172
|
executionSummaryLabel: executionPresentation?.summaryLabel,
|
|
150
|
-
moreActions
|
|
173
|
+
moreActions,
|
|
151
174
|
},
|
|
152
175
|
parts: adaptNcpMessagePartsForChat(message.parts) as ChatMessageSource["parts"],
|
|
153
176
|
};
|
|
@@ -160,6 +183,7 @@ class ChatMessageViewModelAdapter {
|
|
|
160
183
|
language,
|
|
161
184
|
processSummaryLabel,
|
|
162
185
|
executionPresentationKey,
|
|
186
|
+
triggerDetailsKey,
|
|
163
187
|
viewModel,
|
|
164
188
|
});
|
|
165
189
|
return viewModel;
|
|
@@ -295,17 +319,22 @@ export function ChatMessageListContainer({
|
|
|
295
319
|
() => buildChatMessageExecutionLabels(language),
|
|
296
320
|
[language],
|
|
297
321
|
);
|
|
322
|
+
const triggerLabels = useMemo(
|
|
323
|
+
() => buildChatMessageTriggerLabels(language),
|
|
324
|
+
[language],
|
|
325
|
+
);
|
|
298
326
|
const adaptedMessages = useMemo(
|
|
299
327
|
() =>
|
|
300
328
|
chatMessageViewModelAdapter.adapt({
|
|
301
329
|
continuationRunning: isSending,
|
|
302
330
|
executionLabels,
|
|
331
|
+
triggerLabels,
|
|
303
332
|
language,
|
|
304
333
|
processedLabel: t("chatProcessSummaryProcessed"),
|
|
305
334
|
rawMessages,
|
|
306
335
|
texts,
|
|
307
336
|
}),
|
|
308
|
-
[executionLabels, isSending, language, rawMessages, texts],
|
|
337
|
+
[executionLabels, isSending, language, rawMessages, texts, triggerLabels],
|
|
309
338
|
);
|
|
310
339
|
const { handleMessageAction, messages, renderMessageContent } =
|
|
311
340
|
useChatMessageActions({
|
package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts
CHANGED
|
@@ -154,6 +154,12 @@ it("uses structured edit-file result line numbers after the tool finishes", () =
|
|
|
154
154
|
},
|
|
155
155
|
},
|
|
156
156
|
});
|
|
157
|
+
expect(adapted[0]?.parts[0]).not.toMatchObject({
|
|
158
|
+
type: "tool-card",
|
|
159
|
+
card: {
|
|
160
|
+
outputData: expect.anything(),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
157
163
|
});
|
|
158
164
|
|
|
159
165
|
it("builds write-file previews from partial native args before the JSON is complete", () => {
|
|
@@ -261,6 +267,12 @@ it("keeps completed write-file cards in preview mode instead of falling back to
|
|
|
261
267
|
output: "Wrote 3906 bytes to games/snake.html",
|
|
262
268
|
},
|
|
263
269
|
});
|
|
270
|
+
expect(adapted[0]?.parts[0]).not.toMatchObject({
|
|
271
|
+
type: "tool-card",
|
|
272
|
+
card: {
|
|
273
|
+
outputData: expect.anything(),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
264
276
|
});
|
|
265
277
|
|
|
266
278
|
it("renders codex file_change results as structured diff previews", () => {
|
|
@@ -337,4 +349,178 @@ it("renders codex file_change results as structured diff previews", () => {
|
|
|
337
349
|
},
|
|
338
350
|
},
|
|
339
351
|
});
|
|
352
|
+
expect(adapted[0]?.parts[0]).not.toMatchObject({
|
|
353
|
+
type: "tool-card",
|
|
354
|
+
card: {
|
|
355
|
+
outputData: expect.anything(),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("uses a read-file preview as the single visible representation of file content", () => {
|
|
361
|
+
const adapted = adapt([
|
|
362
|
+
{
|
|
363
|
+
id: "assistant-read-result",
|
|
364
|
+
role: "assistant",
|
|
365
|
+
parts: [
|
|
366
|
+
{
|
|
367
|
+
type: "tool-invocation",
|
|
368
|
+
toolInvocation: {
|
|
369
|
+
status: ToolInvocationStatus.RESULT,
|
|
370
|
+
toolCallId: "read-result-1",
|
|
371
|
+
toolName: "read_file",
|
|
372
|
+
args: JSON.stringify({ path: "docs/readme.md" }),
|
|
373
|
+
parsedArgs: { path: "docs/readme.md" },
|
|
374
|
+
result: "1: first line\n2: second line\n</content>",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
] as unknown as ChatMessageSource[]);
|
|
380
|
+
|
|
381
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
382
|
+
type: "tool-card",
|
|
383
|
+
card: {
|
|
384
|
+
toolName: "read_file",
|
|
385
|
+
summary: "docs/readme.md",
|
|
386
|
+
fileOperation: {
|
|
387
|
+
blocks: [
|
|
388
|
+
{
|
|
389
|
+
display: "preview",
|
|
390
|
+
path: "docs/readme.md",
|
|
391
|
+
lines: expect.arrayContaining([
|
|
392
|
+
expect.objectContaining({ text: "1: first line" }),
|
|
393
|
+
expect.objectContaining({ text: "2: second line" }),
|
|
394
|
+
]),
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
expect(adapted[0]?.parts[0]).not.toMatchObject({
|
|
401
|
+
type: "tool-card",
|
|
402
|
+
card: {
|
|
403
|
+
outputData: expect.anything(),
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("uses an apply-patch preview as the single visible representation of the patch", () => {
|
|
409
|
+
const patch = [
|
|
410
|
+
"*** Begin Patch",
|
|
411
|
+
"*** Update File: src/app.ts",
|
|
412
|
+
"@@",
|
|
413
|
+
"-const color = 'red';",
|
|
414
|
+
"+const color = 'blue';",
|
|
415
|
+
"*** End Patch",
|
|
416
|
+
].join("\n");
|
|
417
|
+
const adapted = adapt([
|
|
418
|
+
{
|
|
419
|
+
id: "assistant-apply-patch-result",
|
|
420
|
+
role: "assistant",
|
|
421
|
+
parts: [
|
|
422
|
+
{
|
|
423
|
+
type: "tool-invocation",
|
|
424
|
+
toolInvocation: {
|
|
425
|
+
status: ToolInvocationStatus.RESULT,
|
|
426
|
+
toolCallId: "apply-patch-result-1",
|
|
427
|
+
toolName: "apply_patch",
|
|
428
|
+
args: patch,
|
|
429
|
+
result: "Done!",
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
] as unknown as ChatMessageSource[]);
|
|
435
|
+
|
|
436
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
437
|
+
type: "tool-card",
|
|
438
|
+
card: {
|
|
439
|
+
toolName: "apply_patch",
|
|
440
|
+
summary: "src/app.ts",
|
|
441
|
+
fileOperation: {
|
|
442
|
+
blocks: [
|
|
443
|
+
{
|
|
444
|
+
path: "src/app.ts",
|
|
445
|
+
lines: [
|
|
446
|
+
expect.objectContaining({ kind: "remove", text: "const color = 'red';" }),
|
|
447
|
+
expect.objectContaining({ kind: "add", text: "const color = 'blue';" }),
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
expect(adapted[0]?.parts[0]).not.toMatchObject({
|
|
455
|
+
type: "tool-card",
|
|
456
|
+
card: {
|
|
457
|
+
outputData: expect.anything(),
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("keeps raw file-tool results when no structured preview can be built", () => {
|
|
463
|
+
const adapted = adapt([
|
|
464
|
+
{
|
|
465
|
+
id: "assistant-read-result-without-path",
|
|
466
|
+
role: "assistant",
|
|
467
|
+
parts: [
|
|
468
|
+
{
|
|
469
|
+
type: "tool-invocation",
|
|
470
|
+
toolInvocation: {
|
|
471
|
+
status: ToolInvocationStatus.RESULT,
|
|
472
|
+
toolCallId: "read-result-without-path-1",
|
|
473
|
+
toolName: "read_file",
|
|
474
|
+
result: "raw file content",
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
] as unknown as ChatMessageSource[]);
|
|
480
|
+
|
|
481
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
482
|
+
type: "tool-card",
|
|
483
|
+
card: {
|
|
484
|
+
toolName: "read_file",
|
|
485
|
+
outputData: "raw file content",
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("keeps file-tool errors visible even when input can build a preview", () => {
|
|
491
|
+
const adapted = adapt([
|
|
492
|
+
{
|
|
493
|
+
id: "assistant-write-error",
|
|
494
|
+
role: "assistant",
|
|
495
|
+
parts: [
|
|
496
|
+
{
|
|
497
|
+
type: "tool-invocation",
|
|
498
|
+
toolInvocation: {
|
|
499
|
+
status: ToolInvocationStatus.RESULT,
|
|
500
|
+
toolCallId: "write-error-1",
|
|
501
|
+
toolName: "write_file",
|
|
502
|
+
parsedArgs: {
|
|
503
|
+
path: "src/app.ts",
|
|
504
|
+
content: "const color = 'blue';",
|
|
505
|
+
},
|
|
506
|
+
error: "Permission denied",
|
|
507
|
+
result: "write failed",
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
},
|
|
512
|
+
] as unknown as ChatMessageSource[]);
|
|
513
|
+
|
|
514
|
+
expect(adapted[0]?.parts[0]).toMatchObject({
|
|
515
|
+
type: "tool-card",
|
|
516
|
+
card: {
|
|
517
|
+
toolName: "write_file",
|
|
518
|
+
statusTone: "error",
|
|
519
|
+
output: "Permission denied",
|
|
520
|
+
outputData: "write failed",
|
|
521
|
+
fileOperation: {
|
|
522
|
+
blocks: [expect.objectContaining({ path: "src/app.ts" })],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
});
|
|
340
526
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
buildChatMessageExecutionLabels,
|
|
4
|
+
buildChatMessageTriggerLabels,
|
|
4
5
|
buildChatMessageTexts,
|
|
5
6
|
} from "@/features/chat/features/message/utils/chat-message-texts.utils";
|
|
6
7
|
|
|
@@ -25,6 +26,14 @@ it("provides localized status keys for built-in tool categories", () => {
|
|
|
25
26
|
});
|
|
26
27
|
});
|
|
27
28
|
|
|
29
|
+
it("provides localized labels for run trigger details", () => {
|
|
30
|
+
expect(buildChatMessageTriggerLabels("zh")).toMatchObject({
|
|
31
|
+
viewTrigger: "chatRunTriggerView",
|
|
32
|
+
fields: { sourceModel: "chatRunTriggerSourceModel" },
|
|
33
|
+
actors: { agent: "chatRunTriggerActorAgent" },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
28
37
|
it("provides localized labels for the AI execution metadata action", () => {
|
|
29
38
|
expect(buildChatMessageExecutionLabels("zh")).toMatchObject({
|
|
30
39
|
moreActions: "chatMessageMoreActions",
|
package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts
CHANGED
|
@@ -125,7 +125,9 @@ describe("chat message timeline visibility", () => {
|
|
|
125
125
|
})).toBe(false);
|
|
126
126
|
expect(isVisibleChatMessage(visibleMessage)).toBe(true);
|
|
127
127
|
});
|
|
128
|
+
});
|
|
128
129
|
|
|
130
|
+
describe("chat message continuation projection", () => {
|
|
129
131
|
it("projects continued assistant output into the interrupted message", () => {
|
|
130
132
|
const messages = projectVisibleChatMessages([
|
|
131
133
|
{
|
|
@@ -253,3 +255,72 @@ describe("chat message timeline visibility", () => {
|
|
|
253
255
|
]).map((message) => message.id)).toEqual(["user-1", "assistant-1"]);
|
|
254
256
|
});
|
|
255
257
|
});
|
|
258
|
+
|
|
259
|
+
describe("child session timeline visibility", () => {
|
|
260
|
+
it("does not repeat inherited compaction markers", () => {
|
|
261
|
+
const childMessage = {
|
|
262
|
+
...visibleMessage,
|
|
263
|
+
id: "child-user",
|
|
264
|
+
role: "user" as const,
|
|
265
|
+
timestamp: "2026-08-24T15:50:23.415Z",
|
|
266
|
+
parts: [{ type: "text" as const, text: "child request" }],
|
|
267
|
+
};
|
|
268
|
+
const inheritedCompactions = Array.from({ length: 5 }, (_, index) => ({
|
|
269
|
+
...visibleMessage,
|
|
270
|
+
id: `child-session:inherited:${index + 1}`,
|
|
271
|
+
role: "service" as const,
|
|
272
|
+
timestamp: `2026-08-24T15:0${index}:00.000Z`,
|
|
273
|
+
parts: [{ type: "text" as const, text: "Earlier context was auto-compacted" }],
|
|
274
|
+
metadata: {
|
|
275
|
+
inherited_from_session_id: "parent-session",
|
|
276
|
+
inherited_from_message_id: `parent-compaction-${index + 1}`,
|
|
277
|
+
nextclaw_timeline_kind: "context_compaction",
|
|
278
|
+
checkpoint: {
|
|
279
|
+
id: "parent-checkpoint",
|
|
280
|
+
status: "compressed",
|
|
281
|
+
summary: "Compressed parent context",
|
|
282
|
+
coveredMessageCount: (index + 1) * 100,
|
|
283
|
+
coveredSessionMessageCount: (index + 1) * 100,
|
|
284
|
+
originalEstimatedTokens: 76_000,
|
|
285
|
+
projectedEstimatedTokens: 51_000,
|
|
286
|
+
createdAt: "2026-08-24T15:00:00.000Z",
|
|
287
|
+
updatedAt: `2026-08-24T15:0${index}:00.000Z`,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
} satisfies NcpMessage));
|
|
291
|
+
const items = buildChatMessageTimelineItems({
|
|
292
|
+
rawMessages: [...inheritedCompactions, childMessage],
|
|
293
|
+
messages: [{ id: childMessage.id } as never],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(items.map((item) => item.kind)).toEqual([
|
|
297
|
+
"context-inheritance",
|
|
298
|
+
"message",
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const childCompaction = {
|
|
302
|
+
...inheritedCompactions[0]!,
|
|
303
|
+
id: "child-compaction",
|
|
304
|
+
metadata: {
|
|
305
|
+
...inheritedCompactions[0]!.metadata,
|
|
306
|
+
inherited_from_session_id: undefined,
|
|
307
|
+
inherited_from_message_id: undefined,
|
|
308
|
+
},
|
|
309
|
+
} satisfies NcpMessage;
|
|
310
|
+
const childAssistant = {
|
|
311
|
+
...visibleMessage,
|
|
312
|
+
id: "child-assistant",
|
|
313
|
+
timestamp: "2026-08-24T15:51:00.000Z",
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
expect(buildChatMessageTimelineItems({
|
|
317
|
+
rawMessages: [...inheritedCompactions, childMessage, childCompaction, childAssistant],
|
|
318
|
+
messages: [{ id: childMessage.id } as never, { id: childAssistant.id } as never],
|
|
319
|
+
}).map((item) => item.kind)).toEqual([
|
|
320
|
+
"context-inheritance",
|
|
321
|
+
"message",
|
|
322
|
+
"compaction",
|
|
323
|
+
"message",
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
});
|