@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildChatMessageTriggerDetails,
|
|
4
|
+
mergeChatMessageMoreActions,
|
|
5
|
+
} from "@/features/chat/features/message/utils/chat-message-trigger-details.utils";
|
|
6
|
+
|
|
7
|
+
const labels = {
|
|
8
|
+
moreActions: "More actions",
|
|
9
|
+
viewTrigger: "View trigger details",
|
|
10
|
+
title: "Run trigger",
|
|
11
|
+
description: "Why this run started.",
|
|
12
|
+
close: "Close",
|
|
13
|
+
notAvailable: "Not available",
|
|
14
|
+
fields: {
|
|
15
|
+
actor: "Actor",
|
|
16
|
+
source: "Source",
|
|
17
|
+
triggeredAt: "Triggered at",
|
|
18
|
+
targetRunId: "Target run",
|
|
19
|
+
sourceSessionId: "Source session",
|
|
20
|
+
sourceMessageId: "Source message",
|
|
21
|
+
sourceRunId: "Source run",
|
|
22
|
+
sourceToolCallId: "Source tool call",
|
|
23
|
+
sourceRequestId: "Source request",
|
|
24
|
+
sourceModel: "Source model",
|
|
25
|
+
targetModel: "Run model",
|
|
26
|
+
sourceContext: "Source context",
|
|
27
|
+
raw: "Raw metadata",
|
|
28
|
+
},
|
|
29
|
+
actors: {
|
|
30
|
+
human: "Human",
|
|
31
|
+
agent: "Agent",
|
|
32
|
+
automation: "Automation",
|
|
33
|
+
system: "System",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("buildChatMessageTriggerDetails", () => {
|
|
38
|
+
it("projects structured and raw provenance rows", () => {
|
|
39
|
+
const details = buildChatMessageTriggerDetails({
|
|
40
|
+
labels,
|
|
41
|
+
message: {
|
|
42
|
+
metadata: {
|
|
43
|
+
run_trigger: {
|
|
44
|
+
version: 1,
|
|
45
|
+
actor: "agent",
|
|
46
|
+
source: "sessions_spawn",
|
|
47
|
+
triggeredAt: "2026-08-25T00:00:00.000Z",
|
|
48
|
+
targetRunId: "target-run",
|
|
49
|
+
sourceSessionId: "source-session",
|
|
50
|
+
sourceMessageId: "source-message",
|
|
51
|
+
sourceRunId: "source-run",
|
|
52
|
+
sourceToolCallId: "source-tool-call",
|
|
53
|
+
sourceRequestId: "source-request",
|
|
54
|
+
sourceModel: "openai/gpt-5.6",
|
|
55
|
+
sourceContext: { channel: "telegram", chatId: "chat-1" },
|
|
56
|
+
},
|
|
57
|
+
run_spec: {
|
|
58
|
+
runId: "target-run",
|
|
59
|
+
model: "minimax/MiniMax-M3",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(details?.triggerLabel).toBe("More actions");
|
|
66
|
+
expect(details?.items[0]?.dialog.rows).toEqual(expect.arrayContaining([
|
|
67
|
+
{ label: "Actor", value: "Agent" },
|
|
68
|
+
{ label: "Source model", value: "openai/gpt-5.6" },
|
|
69
|
+
{ label: "Run model", value: "minimax/MiniMax-M3" },
|
|
70
|
+
{ label: "Source context", value: expect.stringContaining("telegram") },
|
|
71
|
+
expect.objectContaining({ label: "Raw metadata" }),
|
|
72
|
+
]));
|
|
73
|
+
expect(details?.items[0]?.dialog.rows.find(
|
|
74
|
+
(row) => row.label === "Raw metadata",
|
|
75
|
+
)?.value).toContain("run_spec");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not create an empty action for legacy messages", () => {
|
|
79
|
+
expect(buildChatMessageTriggerDetails({ labels, message: {} })).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("keeps trigger-only messages actionable and merges assistant details in order", () => {
|
|
83
|
+
const triggerDetails = buildChatMessageTriggerDetails({
|
|
84
|
+
labels,
|
|
85
|
+
message: {
|
|
86
|
+
metadata: {
|
|
87
|
+
run_trigger: {
|
|
88
|
+
version: 1,
|
|
89
|
+
actor: "human",
|
|
90
|
+
source: "ui-http",
|
|
91
|
+
triggeredAt: "2026-08-25T00:00:00.000Z",
|
|
92
|
+
targetRunId: "run-1",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(mergeChatMessageMoreActions(null, triggerDetails)?.items.map(
|
|
98
|
+
(item) => item.key,
|
|
99
|
+
)).toEqual(["run-trigger-metadata"]);
|
|
100
|
+
expect(mergeChatMessageMoreActions({
|
|
101
|
+
triggerLabel: "More actions",
|
|
102
|
+
items: [{
|
|
103
|
+
key: "ai-execution-metadata",
|
|
104
|
+
label: "View run metadata",
|
|
105
|
+
dialog: { title: "Run", closeLabel: "Close", rows: [] },
|
|
106
|
+
}],
|
|
107
|
+
}, triggerDetails)?.items.map((item) => item.key)).toEqual([
|
|
108
|
+
"ai-execution-metadata",
|
|
109
|
+
"run-trigger-metadata",
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("keeps a completed steering user message actionable from persisted run metadata", () => {
|
|
114
|
+
const details = buildChatMessageTriggerDetails({
|
|
115
|
+
labels,
|
|
116
|
+
message: {
|
|
117
|
+
metadata: {
|
|
118
|
+
run_spec: {
|
|
119
|
+
runId: "run-1",
|
|
120
|
+
model: "openai/gpt-5.6",
|
|
121
|
+
},
|
|
122
|
+
run_trigger: {
|
|
123
|
+
version: 1,
|
|
124
|
+
actor: "human",
|
|
125
|
+
source: "internal",
|
|
126
|
+
sourceMessageId: "user-steering",
|
|
127
|
+
targetRunId: "run-1",
|
|
128
|
+
triggeredAt: "2026-08-25T13:39:04.000Z",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(details).toMatchObject({
|
|
135
|
+
triggerLabel: "More actions",
|
|
136
|
+
items: [{
|
|
137
|
+
key: "run-trigger-metadata",
|
|
138
|
+
dialog: {
|
|
139
|
+
rows: expect.arrayContaining([
|
|
140
|
+
{ label: "Target run", value: "run-1" },
|
|
141
|
+
{ label: "Run model", value: "openai/gpt-5.6" },
|
|
142
|
+
]),
|
|
143
|
+
},
|
|
144
|
+
}],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -174,7 +174,7 @@ function buildToolInvocationPart(
|
|
|
174
174
|
detail,
|
|
175
175
|
inputData: fileOperationCardData ? undefined : invocation.parsedArgs ?? invocation.args,
|
|
176
176
|
text: shouldShowRawResult && rawResult ? rawResult : undefined,
|
|
177
|
-
outputData: invocation.result,
|
|
177
|
+
outputData: shouldShowRawResult ? invocation.result : undefined,
|
|
178
178
|
callId: invocation.toolCallId || undefined,
|
|
179
179
|
toolCallId: invocation.toolCallId || undefined,
|
|
180
180
|
...(invocation.execution ? { execution: invocation.execution } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChatMessageTexts } from "@nextclaw/agent-chat-ui";
|
|
2
2
|
import type { ChatMessageExecutionPresentationLabels } from "./chat-message-execution-summary.utils";
|
|
3
|
+
import type { ChatMessageTriggerDetailsLabels } from "./chat-message-trigger-details.utils";
|
|
3
4
|
import type { ChatMessageAdapterTexts } from "@/features/chat/features/message/utils/chat-message.utils";
|
|
4
5
|
import { t } from "@/shared/lib/i18n";
|
|
5
6
|
|
|
@@ -140,6 +141,41 @@ export function buildChatMessageExecutionLabels(
|
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
export function buildChatMessageTriggerLabels(
|
|
145
|
+
language: string,
|
|
146
|
+
): ChatMessageTriggerDetailsLabels {
|
|
147
|
+
void language;
|
|
148
|
+
return {
|
|
149
|
+
moreActions: t("chatMessageMoreActions"),
|
|
150
|
+
viewTrigger: t("chatRunTriggerView"),
|
|
151
|
+
title: t("chatRunTriggerTitle"),
|
|
152
|
+
description: t("chatRunTriggerDescription"),
|
|
153
|
+
close: t("chatRunTriggerClose"),
|
|
154
|
+
notAvailable: t("chatAiExecutionNotAvailable"),
|
|
155
|
+
fields: {
|
|
156
|
+
actor: t("chatRunTriggerActor"),
|
|
157
|
+
source: t("chatRunTriggerSource"),
|
|
158
|
+
triggeredAt: t("chatRunTriggerTime"),
|
|
159
|
+
targetRunId: t("chatRunTriggerTargetRun"),
|
|
160
|
+
sourceSessionId: t("chatRunTriggerSourceSession"),
|
|
161
|
+
sourceMessageId: t("chatRunTriggerSourceMessage"),
|
|
162
|
+
sourceRunId: t("chatRunTriggerSourceRun"),
|
|
163
|
+
sourceToolCallId: t("chatRunTriggerSourceToolCall"),
|
|
164
|
+
sourceRequestId: t("chatRunTriggerSourceRequest"),
|
|
165
|
+
sourceModel: t("chatRunTriggerSourceModel"),
|
|
166
|
+
targetModel: t("chatRunTriggerTargetModel"),
|
|
167
|
+
sourceContext: t("chatRunTriggerSourceContext"),
|
|
168
|
+
raw: t("chatRunTriggerRaw"),
|
|
169
|
+
},
|
|
170
|
+
actors: {
|
|
171
|
+
human: t("chatRunTriggerActorHuman"),
|
|
172
|
+
agent: t("chatRunTriggerActorAgent"),
|
|
173
|
+
automation: t("chatRunTriggerActorAutomation"),
|
|
174
|
+
system: t("chatRunTriggerActorSystem"),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
export function buildChatMessageTexts(
|
|
144
180
|
language: string,
|
|
145
181
|
): ChatMessageTexts {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ChatMessageMoreActionsViewModel } from "@nextclaw/agent-chat-ui";
|
|
2
|
+
import {
|
|
3
|
+
readNcpAiExecutionMetadata,
|
|
4
|
+
readNcpRunTriggerMetadata,
|
|
5
|
+
type NcpMessage,
|
|
6
|
+
type NcpRunTriggerActor,
|
|
7
|
+
} from "@nextclaw/ncp";
|
|
8
|
+
|
|
9
|
+
export type ChatMessageTriggerDetailsLabels = {
|
|
10
|
+
moreActions: string;
|
|
11
|
+
viewTrigger: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
close: string;
|
|
15
|
+
notAvailable: string;
|
|
16
|
+
fields: {
|
|
17
|
+
actor: string;
|
|
18
|
+
source: string;
|
|
19
|
+
triggeredAt: string;
|
|
20
|
+
targetRunId: string;
|
|
21
|
+
sourceSessionId: string;
|
|
22
|
+
sourceMessageId: string;
|
|
23
|
+
sourceRunId: string;
|
|
24
|
+
sourceToolCallId: string;
|
|
25
|
+
sourceRequestId: string;
|
|
26
|
+
sourceModel: string;
|
|
27
|
+
targetModel: string;
|
|
28
|
+
sourceContext: string;
|
|
29
|
+
raw: string;
|
|
30
|
+
};
|
|
31
|
+
actors: Record<NcpRunTriggerActor, string>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function mergeChatMessageMoreActions(
|
|
35
|
+
...candidates: Array<ChatMessageMoreActionsViewModel | null | undefined>
|
|
36
|
+
): ChatMessageMoreActionsViewModel | undefined {
|
|
37
|
+
const available = candidates.filter(
|
|
38
|
+
(candidate): candidate is ChatMessageMoreActionsViewModel =>
|
|
39
|
+
Boolean(candidate?.items.length),
|
|
40
|
+
);
|
|
41
|
+
if (available.length === 0) return undefined;
|
|
42
|
+
return {
|
|
43
|
+
triggerLabel: available[0]!.triggerLabel,
|
|
44
|
+
items: available.flatMap((candidate) => candidate.items),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readRunSpec(
|
|
49
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
50
|
+
): Record<string, unknown> | null {
|
|
51
|
+
const value = metadata?.run_spec;
|
|
52
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
53
|
+
? value as Record<string, unknown>
|
|
54
|
+
: null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readTargetModel(
|
|
58
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
const runSpecModel = readRunSpec(metadata)?.model;
|
|
61
|
+
if (typeof runSpecModel === "string" && runSpecModel.trim()) {
|
|
62
|
+
return runSpecModel.trim();
|
|
63
|
+
}
|
|
64
|
+
return readNcpAiExecutionMetadata(metadata)?.model;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildChatMessageTriggerDetails(params: {
|
|
68
|
+
labels: ChatMessageTriggerDetailsLabels;
|
|
69
|
+
message: Pick<NcpMessage, "metadata">;
|
|
70
|
+
}): ChatMessageMoreActionsViewModel | null {
|
|
71
|
+
const { labels, message } = params;
|
|
72
|
+
const trigger = readNcpRunTriggerMetadata(message.metadata);
|
|
73
|
+
if (!trigger) return null;
|
|
74
|
+
const { fields } = labels;
|
|
75
|
+
const runSpec = readRunSpec(message.metadata);
|
|
76
|
+
const rows = [
|
|
77
|
+
{ label: fields.actor, value: labels.actors[trigger.actor] },
|
|
78
|
+
{ label: fields.source, value: trigger.source },
|
|
79
|
+
{ label: fields.triggeredAt, value: trigger.triggeredAt },
|
|
80
|
+
{ label: fields.targetRunId, value: trigger.targetRunId },
|
|
81
|
+
{ label: fields.sourceSessionId, value: trigger.sourceSessionId },
|
|
82
|
+
{ label: fields.sourceMessageId, value: trigger.sourceMessageId },
|
|
83
|
+
{ label: fields.sourceRunId, value: trigger.sourceRunId },
|
|
84
|
+
{ label: fields.sourceToolCallId, value: trigger.sourceToolCallId },
|
|
85
|
+
{ label: fields.sourceRequestId, value: trigger.sourceRequestId },
|
|
86
|
+
{ label: fields.sourceModel, value: trigger.sourceModel },
|
|
87
|
+
{ label: fields.targetModel, value: readTargetModel(message.metadata) },
|
|
88
|
+
{
|
|
89
|
+
label: fields.sourceContext,
|
|
90
|
+
value: trigger.sourceContext
|
|
91
|
+
? JSON.stringify(trigger.sourceContext, null, 2)
|
|
92
|
+
: undefined,
|
|
93
|
+
},
|
|
94
|
+
].map((row) => ({
|
|
95
|
+
label: row.label,
|
|
96
|
+
value: row.value ?? labels.notAvailable,
|
|
97
|
+
}));
|
|
98
|
+
rows.push({
|
|
99
|
+
label: fields.raw,
|
|
100
|
+
value: JSON.stringify({
|
|
101
|
+
run_trigger: trigger,
|
|
102
|
+
...(runSpec ? { run_spec: runSpec } : {}),
|
|
103
|
+
}, null, 2),
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
triggerLabel: labels.moreActions,
|
|
107
|
+
items: [{
|
|
108
|
+
key: "run-trigger-metadata",
|
|
109
|
+
label: labels.viewTrigger,
|
|
110
|
+
dialog: {
|
|
111
|
+
title: labels.title,
|
|
112
|
+
description: labels.description,
|
|
113
|
+
closeLabel: labels.close,
|
|
114
|
+
rows,
|
|
115
|
+
},
|
|
116
|
+
}],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -73,4 +73,54 @@ describe('useNcpAgentRuntime backend queue submission', () => {
|
|
|
73
73
|
expect(result.current.visibleMessages).toEqual([queuedMessage]);
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it('keeps a steered message in the pending projection instead of accepting it as a new run', async () => {
|
|
78
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
79
|
+
manager.hydrate({ sessionId: 'session-1', messages: [] });
|
|
80
|
+
await manager.dispatch({
|
|
81
|
+
type: NcpEventType.RunStarted,
|
|
82
|
+
payload: {
|
|
83
|
+
sessionId: 'session-1',
|
|
84
|
+
runId: 'run-1',
|
|
85
|
+
messageId: 'assistant-1',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const send = vi.fn(async (envelope: NcpAgentSendEnvelope) => ({
|
|
89
|
+
assistantMessageId: null,
|
|
90
|
+
delivery: 'steered' as const,
|
|
91
|
+
runId: 'run-1',
|
|
92
|
+
sessionId: 'session-1',
|
|
93
|
+
userMessageId: envelope.message.id,
|
|
94
|
+
}));
|
|
95
|
+
const client = {
|
|
96
|
+
abort: vi.fn(async () => undefined),
|
|
97
|
+
send,
|
|
98
|
+
stop: vi.fn(async () => undefined),
|
|
99
|
+
stream: vi.fn(async () => undefined),
|
|
100
|
+
subscribe: vi.fn(() => () => undefined),
|
|
101
|
+
} as unknown as NcpAgentClientEndpoint;
|
|
102
|
+
const { result } = renderHook(() => useNcpAgentRuntime({
|
|
103
|
+
client,
|
|
104
|
+
manager,
|
|
105
|
+
sessionId: 'session-1',
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
await result.current.send({
|
|
110
|
+
delivery: 'prefer-steer',
|
|
111
|
+
message: {
|
|
112
|
+
id: 'user-steering',
|
|
113
|
+
sessionId: 'session-1',
|
|
114
|
+
role: 'user',
|
|
115
|
+
status: 'final',
|
|
116
|
+
parts: [{ type: 'text', text: 'change direction' }],
|
|
117
|
+
timestamp: '2026-08-25T00:00:00.000Z',
|
|
118
|
+
},
|
|
119
|
+
sessionId: 'session-1',
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.current.visibleMessages).toEqual([]);
|
|
124
|
+
expect(result.current.snapshot.activeRun?.runId).toBe('run-1');
|
|
125
|
+
});
|
|
76
126
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
1
|
+
import { act, render, renderHook, waitFor } from "@testing-library/react";
|
|
2
2
|
import {
|
|
3
3
|
type NcpAgentClientEndpoint,
|
|
4
4
|
type NcpAgentSendEnvelope,
|
|
@@ -158,6 +158,29 @@ class ExistingSessionLiveClient implements NcpAgentClientEndpoint {
|
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function RuntimeMessagesProbe({
|
|
162
|
+
client,
|
|
163
|
+
manager,
|
|
164
|
+
}: {
|
|
165
|
+
client: NcpAgentClientEndpoint;
|
|
166
|
+
manager: DefaultNcpAgentConversationStateManager;
|
|
167
|
+
}) {
|
|
168
|
+
const { visibleMessages } = useNcpAgentRuntime({
|
|
169
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
170
|
+
client,
|
|
171
|
+
manager,
|
|
172
|
+
});
|
|
173
|
+
return (
|
|
174
|
+
<div data-testid="runtime-messages">
|
|
175
|
+
{visibleMessages.map((message) => (
|
|
176
|
+
<div key={message.id} data-message-id={message.id}>
|
|
177
|
+
{message.id}
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
161
184
|
function readAssistantText(messages: readonly NcpMessage[]): string {
|
|
162
185
|
const assistant = messages.find((message) => message.role === "assistant");
|
|
163
186
|
return assistant?.parts
|
|
@@ -421,6 +444,72 @@ describe("useNcpAgentRuntime", () => {
|
|
|
421
444
|
]);
|
|
422
445
|
});
|
|
423
446
|
|
|
447
|
+
it("keeps the live assistant boundary and DOM identity when a steered user has an earlier timestamp", async () => {
|
|
448
|
+
const client = new DeferredSendClient();
|
|
449
|
+
const manager = new DefaultNcpAgentConversationStateManager();
|
|
450
|
+
manager.hydrate({
|
|
451
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
452
|
+
messages: [{
|
|
453
|
+
id: "user-mt7jp7vd",
|
|
454
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
455
|
+
role: "user",
|
|
456
|
+
status: "final",
|
|
457
|
+
parts: [{ type: "text", text: "first" }],
|
|
458
|
+
timestamp: "2026-08-24T18:04:10.460Z",
|
|
459
|
+
}],
|
|
460
|
+
});
|
|
461
|
+
await manager.dispatchBatch([
|
|
462
|
+
{
|
|
463
|
+
type: NcpEventType.RunStarted,
|
|
464
|
+
payload: {
|
|
465
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
466
|
+
runId: "agent-run-7c65d8b1-6bda-4f08-a2ad-d5f5318366fb",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
type: NcpEventType.MessageReasoningDelta,
|
|
471
|
+
payload: {
|
|
472
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
473
|
+
messageId: "assistant-message-2629eaf9-c4ce-4562-b22d-c70f1e8c85e2",
|
|
474
|
+
delta: "Planning",
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
|
|
479
|
+
const view = render(<RuntimeMessagesProbe client={client} manager={manager} />);
|
|
480
|
+
const assistantSelector = '[data-message-id="assistant-message-2629eaf9-c4ce-4562-b22d-c70f1e8c85e2"]';
|
|
481
|
+
const assistantNode = view.container.querySelector(assistantSelector);
|
|
482
|
+
expect(assistantNode).not.toBeNull();
|
|
483
|
+
|
|
484
|
+
await act(async () => {
|
|
485
|
+
await manager.dispatch({
|
|
486
|
+
type: NcpEventType.MessageSent,
|
|
487
|
+
payload: {
|
|
488
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
489
|
+
message: {
|
|
490
|
+
id: "user-mt7jpad5",
|
|
491
|
+
sessionId: "ncp-mt7hs0u9-15yce6jv",
|
|
492
|
+
role: "user",
|
|
493
|
+
status: "final",
|
|
494
|
+
parts: [{ type: "text", text: "steer" }],
|
|
495
|
+
timestamp: "2026-08-24T18:04:13.625Z",
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
await waitFor(() => {
|
|
502
|
+
const ids = [...view.container.querySelectorAll("[data-message-id]")]
|
|
503
|
+
.map((node) => node.getAttribute("data-message-id"));
|
|
504
|
+
expect(ids).toEqual([
|
|
505
|
+
"user-mt7jp7vd",
|
|
506
|
+
"assistant-message-2629eaf9-c4ce-4562-b22d-c70f1e8c85e2",
|
|
507
|
+
"user-mt7jpad5",
|
|
508
|
+
]);
|
|
509
|
+
});
|
|
510
|
+
expect(view.container.querySelector(assistantSelector)).toBe(assistantNode);
|
|
511
|
+
});
|
|
512
|
+
|
|
424
513
|
});
|
|
425
514
|
|
|
426
515
|
describe("useNcpAgentRuntime commands and live stream", () => {
|
|
@@ -62,13 +62,34 @@ describe("useNcpSessionListView", () => {
|
|
|
62
62
|
it("can override the hidden sidebar query for header switching", () => {
|
|
63
63
|
useChatSessionListStore.getState().setSnapshot({ query: "Alpha" });
|
|
64
64
|
|
|
65
|
-
const { result } = renderHook(() =>
|
|
66
|
-
useNcpSessionListView({ query: "" }),
|
|
67
|
-
);
|
|
65
|
+
const { result } = renderHook(() => useNcpSessionListView({ query: "" }));
|
|
68
66
|
|
|
69
67
|
expect(result.current.items.map((item) => item.session.key)).toEqual([
|
|
70
68
|
"session:alpha",
|
|
71
69
|
"session:beta",
|
|
72
70
|
]);
|
|
73
71
|
});
|
|
72
|
+
|
|
73
|
+
it("keeps hidden child sessions available for sidebar context counts", () => {
|
|
74
|
+
mocks.sessions.push({
|
|
75
|
+
...createSummary("session:alpha:child", "Child Task"),
|
|
76
|
+
metadata: {
|
|
77
|
+
label: "Child Task",
|
|
78
|
+
session_type: "native",
|
|
79
|
+
parent_session_id: "session:alpha",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useNcpSessionListView());
|
|
84
|
+
|
|
85
|
+
expect(result.current.items.map((item) => item.session.key)).toEqual([
|
|
86
|
+
"session:alpha",
|
|
87
|
+
"session:beta",
|
|
88
|
+
]);
|
|
89
|
+
expect(result.current.allItems.map((item) => item.session.key)).toEqual([
|
|
90
|
+
"session:alpha",
|
|
91
|
+
"session:beta",
|
|
92
|
+
"session:alpha:child",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
74
95
|
});
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { useMemo } from
|
|
2
|
-
import type { SessionEntryView } from
|
|
3
|
-
import { sessionMatchesQuery } from
|
|
4
|
-
import { adaptNcpSessionSummaries } from
|
|
5
|
-
import { useChatSessionListStore } from
|
|
6
|
-
import { useNcpSessions } from
|
|
7
|
-
import type { SessionRunStatus } from
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { SessionEntryView } from "@/shared/lib/api";
|
|
3
|
+
import { sessionMatchesQuery } from "@/features/chat/features/session/utils/chat-session-display.utils";
|
|
4
|
+
import { adaptNcpSessionSummaries } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
5
|
+
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
6
|
+
import { useNcpSessions } from "@/features/chat/features/ncp/hooks/use-ncp-session-queries";
|
|
7
|
+
import type { SessionRunStatus } from "@/features/chat/types/session-run-status.types";
|
|
8
8
|
|
|
9
9
|
export type NcpSessionListItemView = {
|
|
10
10
|
session: SessionEntryView;
|
|
11
11
|
runStatus?: SessionRunStatus;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
function filterSessionsByQuery(
|
|
14
|
+
function filterSessionsByQuery(
|
|
15
|
+
sessions: readonly SessionEntryView[],
|
|
16
|
+
query: string,
|
|
17
|
+
): SessionEntryView[] {
|
|
15
18
|
return sessions.filter((session) => sessionMatchesQuery(session, query));
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -22,26 +25,39 @@ function shouldShowSessionInSidebar(session: SessionEntryView): boolean {
|
|
|
22
25
|
return session.isPromotedChildSession === true;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function useNcpSessionListView(
|
|
28
|
+
export function useNcpSessionListView(
|
|
29
|
+
params: { limit?: number; query?: string | null } = {},
|
|
30
|
+
) {
|
|
26
31
|
const storedQuery = useChatSessionListStore((state) => state.snapshot.query);
|
|
27
32
|
const query = params.query ?? storedQuery;
|
|
28
33
|
const sessionsQuery = useNcpSessions({ limit: params.limit ?? 200 });
|
|
29
34
|
|
|
30
|
-
const
|
|
35
|
+
const allItems = useMemo<NcpSessionListItemView[]>(() => {
|
|
31
36
|
const summaries = sessionsQuery.data?.sessions ?? [];
|
|
32
|
-
|
|
33
|
-
shouldShowSessionInSidebar,
|
|
34
|
-
);
|
|
35
|
-
const filteredSessions = filterSessionsByQuery(sessions, query);
|
|
36
|
-
|
|
37
|
-
return filteredSessions.map((session) => ({
|
|
37
|
+
return adaptNcpSessionSummaries(summaries).map((session) => ({
|
|
38
38
|
session,
|
|
39
|
-
runStatus: session.status ===
|
|
39
|
+
runStatus: session.status === "running" ? "running" : undefined,
|
|
40
40
|
}));
|
|
41
|
-
}, [
|
|
41
|
+
}, [sessionsQuery.data?.sessions]);
|
|
42
|
+
const items = useMemo<NcpSessionListItemView[]>(() => {
|
|
43
|
+
const visibleItems = allItems.filter(({ session }) =>
|
|
44
|
+
shouldShowSessionInSidebar(session),
|
|
45
|
+
);
|
|
46
|
+
const matchingSessionKeys = new Set(
|
|
47
|
+
filterSessionsByQuery(
|
|
48
|
+
visibleItems.map(({ session }) => session),
|
|
49
|
+
query,
|
|
50
|
+
).map((session) => session.key),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return visibleItems.filter(({ session }) =>
|
|
54
|
+
matchingSessionKeys.has(session.key),
|
|
55
|
+
);
|
|
56
|
+
}, [allItems, query]);
|
|
42
57
|
|
|
43
58
|
return {
|
|
59
|
+
allItems,
|
|
44
60
|
isLoading: sessionsQuery.isLoading,
|
|
45
|
-
items
|
|
61
|
+
items,
|
|
46
62
|
};
|
|
47
63
|
}
|
package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen, within } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
2
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
4
|
import { ChatSidebarProjectGroups } from "@/features/chat/features/session/components/chat-sidebar-project-groups";
|
|
4
5
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
@@ -38,6 +39,7 @@ function renderProjectGroups(isPinned = false) {
|
|
|
38
39
|
return render(
|
|
39
40
|
<ChatSidebarProjectGroups
|
|
40
41
|
groups={[{ ...projectGroup, isPinned }]}
|
|
42
|
+
projectCronJobCountByRoot={new Map([[projectGroup.projectRoot, 2]])}
|
|
41
43
|
defaultSessionType="native"
|
|
42
44
|
sessionTypeOptions={[
|
|
43
45
|
{ value: "native", label: "Native", icon: null, ready: true },
|
|
@@ -74,7 +76,15 @@ describe("ChatSidebarProjectGroups", () => {
|
|
|
74
76
|
expect(header.parentElement?.className).toContain("h-8");
|
|
75
77
|
expect(header.parentElement?.className).toContain("hover:bg-gray-200/60");
|
|
76
78
|
expect(header.parentElement?.className).not.toContain("focus-within:bg");
|
|
77
|
-
expect(
|
|
79
|
+
expect(
|
|
80
|
+
screen.getByText("analysis-project").nextElementSibling?.tagName,
|
|
81
|
+
).toBe("svg");
|
|
82
|
+
expect(
|
|
83
|
+
screen
|
|
84
|
+
.getByText("analysis-project")
|
|
85
|
+
.nextElementSibling?.getAttribute("class"),
|
|
86
|
+
).toContain("opacity-0");
|
|
87
|
+
expect(screen.queryByText("Sessions")).toBeNull();
|
|
78
88
|
|
|
79
89
|
fireEvent.click(header);
|
|
80
90
|
|
|
@@ -87,6 +97,20 @@ describe("ChatSidebarProjectGroups", () => {
|
|
|
87
97
|
expect(screen.queryByText("Project session")).toBeNull();
|
|
88
98
|
});
|
|
89
99
|
|
|
100
|
+
it("shows project context from the row on hover", async () => {
|
|
101
|
+
const user = userEvent.setup();
|
|
102
|
+
renderProjectGroups();
|
|
103
|
+
|
|
104
|
+
await user.hover(screen.getByLabelText("Collapse project"));
|
|
105
|
+
|
|
106
|
+
const tooltip = await screen.findByRole("tooltip");
|
|
107
|
+
expect(within(tooltip).getByText("Path")).toBeTruthy();
|
|
108
|
+
expect(within(tooltip).getByText("Sessions")).toBeTruthy();
|
|
109
|
+
expect(within(tooltip).getByText("Scheduled tasks")).toBeTruthy();
|
|
110
|
+
expect(within(tooltip).getByText("/tmp/analysis-project")).toBeTruthy();
|
|
111
|
+
expect(within(tooltip).getByText("2")).toBeTruthy();
|
|
112
|
+
});
|
|
113
|
+
|
|
90
114
|
it("keeps project creation and pinning in one trailing action cluster", () => {
|
|
91
115
|
renderProjectGroups();
|
|
92
116
|
|