@nextclaw/ui 0.11.22 → 0.11.23
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 +10 -0
- package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-DVDu1xvz.js} +1 -1
- package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
- package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-Buo9HrOz.js} +1 -1
- package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
- package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-JnkYwK7p.js} +1 -1
- package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-BYRhgp0c.js} +1 -1
- package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-DmLyyHvX.js} +1 -1
- package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-CDSSvH7Z.js} +1 -1
- package/dist/assets/{RuntimeConfig-Cmn2xPQO.js → RuntimeConfig-v7a7Fe3x.js} +1 -1
- package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-D5f1EkLE.js} +1 -1
- package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-D61IKcYt.js} +1 -1
- package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-BRIxVTEv.js} +2 -2
- package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
- package/dist/assets/{index-C6d0xmtm.js → index-BuwbBgmT.js} +2 -2
- package/dist/assets/index-bZ8cqQIS.css +1 -0
- package/dist/assets/{security-config-T5zpg16O.js → security-config-DbUyWcQz.js} +1 -1
- package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-COwYXDKm.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/api/types.ts +4 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +10 -2
- package/src/components/chat/ChatConversationPanel.tsx +114 -77
- package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -5
- package/src/components/chat/adapters/chat-message.adapter.test.ts +83 -9
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
- package/src/components/chat/chat-child-session-panel.tsx +100 -0
- package/src/components/chat/chat-page-runtime.test.ts +1 -0
- package/src/components/chat/chat-session-display.test.ts +1 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
- package/src/components/chat/ncp/NcpChatPage.tsx +179 -114
- package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +21 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +31 -0
- package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
- package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
- package/src/components/chat/stores/chat-thread.store.ts +11 -1
- package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
- package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
- package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
- package/dist/assets/index-BlH4-cBw.css +0 -1
- package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +0 -154
|
@@ -39,9 +39,30 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
39
39
|
projectName: 'project-alpha',
|
|
40
40
|
sessionType: 'native',
|
|
41
41
|
sessionTypeMutable: false,
|
|
42
|
+
isChildSession: false,
|
|
42
43
|
messageCount: 3
|
|
43
44
|
});
|
|
44
45
|
});
|
|
46
|
+
|
|
47
|
+
it('marks child sessions from parent_session_id metadata and keeps the request link', () => {
|
|
48
|
+
const adapted = adaptNcpSessionSummary(
|
|
49
|
+
createSummary({
|
|
50
|
+
metadata: {
|
|
51
|
+
label: 'Verifier',
|
|
52
|
+
session_type: 'native',
|
|
53
|
+
parent_session_id: 'parent-session-1',
|
|
54
|
+
spawned_by_request_id: 'request-1',
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(adapted).toMatchObject({
|
|
60
|
+
key: 'ncp-session-1',
|
|
61
|
+
isChildSession: true,
|
|
62
|
+
parentSessionId: 'parent-session-1',
|
|
63
|
+
spawnedByRequestId: 'request-1',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
45
66
|
});
|
|
46
67
|
|
|
47
68
|
describe('adaptNcpMessageToUiMessage', () => {
|
|
@@ -88,6 +88,30 @@ function readNcpSessionType(summary: NcpSessionSummaryView): string {
|
|
|
88
88
|
return readOptionalString(metadata.session_type) ?? readOptionalString(metadata.sessionType) ?? 'native';
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function readNcpParentSessionId(summary: NcpSessionSummaryView): string | null {
|
|
92
|
+
const metadata = readMetadata(summary);
|
|
93
|
+
if (!metadata) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return readOptionalString(metadata.parent_session_id) ?? readOptionalString(metadata.parentSessionId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readNcpSpawnedByRequestId(summary: NcpSessionSummaryView): string | null {
|
|
100
|
+
const metadata = readMetadata(summary);
|
|
101
|
+
if (!metadata) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return readOptionalString(metadata.spawned_by_request_id) ?? readOptionalString(metadata.spawnedByRequestId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readPromotedChildSession(summary: NcpSessionSummaryView): boolean {
|
|
108
|
+
const metadata = readMetadata(summary);
|
|
109
|
+
if (!metadata) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return metadata.child_session_promoted === true;
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
function parseSessionContext(sessionKey: string): { channel?: string; type?: string } {
|
|
92
116
|
if (sessionKey === 'heartbeat') {
|
|
93
117
|
return { type: 'heartbeat' };
|
|
@@ -222,6 +246,9 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
222
246
|
const projectRoot = readNcpSessionProjectRoot(summary);
|
|
223
247
|
const projectName = getSessionProjectName(projectRoot);
|
|
224
248
|
const context = parseSessionContext(summary.sessionId);
|
|
249
|
+
const parentSessionId = readNcpParentSessionId(summary);
|
|
250
|
+
const spawnedByRequestId = readNcpSpawnedByRequestId(summary);
|
|
251
|
+
const isPromotedChildSession = readPromotedChildSession(summary);
|
|
225
252
|
return {
|
|
226
253
|
key: summary.sessionId,
|
|
227
254
|
createdAt: summary.updatedAt,
|
|
@@ -234,6 +261,10 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
|
|
|
234
261
|
...(projectName ? { projectName } : {}),
|
|
235
262
|
sessionType: readNcpSessionType(summary),
|
|
236
263
|
sessionTypeMutable: false,
|
|
264
|
+
isChildSession: Boolean(parentSessionId),
|
|
265
|
+
...(isPromotedChildSession ? { isPromotedChildSession } : {}),
|
|
266
|
+
...(parentSessionId ? { parentSessionId } : {}),
|
|
267
|
+
...(spawnedByRequestId ? { spawnedByRequestId } : {}),
|
|
237
268
|
messageCount: summary.messageCount
|
|
238
269
|
};
|
|
239
270
|
}
|
|
@@ -15,13 +15,22 @@ function filterSessionsByQuery(sessions: readonly SessionEntryView[], query: str
|
|
|
15
15
|
return sessions.filter((session) => sessionMatchesQuery(session, query));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function shouldShowSessionInSidebar(session: SessionEntryView): boolean {
|
|
19
|
+
if (!session.isChildSession) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return session.isPromotedChildSession === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
export function useNcpSessionListView(params: { limit?: number } = {}) {
|
|
19
26
|
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
20
27
|
const sessionsQuery = useNcpSessions({ limit: params.limit ?? 200 });
|
|
21
28
|
|
|
22
29
|
const items = useMemo<NcpSessionListItemView[]>(() => {
|
|
23
30
|
const summaries = sessionsQuery.data?.sessions ?? [];
|
|
24
|
-
const sessions = adaptNcpSessionSummaries(summaries)
|
|
31
|
+
const sessions = adaptNcpSessionSummaries(summaries).filter(
|
|
32
|
+
shouldShowSessionInSidebar,
|
|
33
|
+
);
|
|
25
34
|
const filteredSessions = filterSessionsByQuery(sessions, query);
|
|
26
35
|
const summaryBySessionId = new Map(summaries.map((summary) => [summary.sessionId, summary]));
|
|
27
36
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
1
|
+
import type { ChatComposerNode, ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
|
|
2
2
|
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
3
3
|
import { createContext, useContext } from 'react';
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
@@ -37,6 +37,9 @@ export type ChatThreadManagerLike = {
|
|
|
37
37
|
deleteSession: () => void;
|
|
38
38
|
createSession: () => void;
|
|
39
39
|
goToProviders: () => void;
|
|
40
|
+
openSessionFromToolAction: (action: ChatToolActionViewModel) => void;
|
|
41
|
+
closeChildSessionDetail: () => void;
|
|
42
|
+
goToParentSession: () => void;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
export type ChatPresenterLike = {
|
|
@@ -20,6 +20,11 @@ export type ChatThreadSnapshot = {
|
|
|
20
20
|
messages: readonly NcpMessage[];
|
|
21
21
|
isSending: boolean;
|
|
22
22
|
isAwaitingAssistantOutput: boolean;
|
|
23
|
+
parentSessionKey?: string | null;
|
|
24
|
+
parentSessionLabel?: string | null;
|
|
25
|
+
childSessionDetailSessionKey?: string | null;
|
|
26
|
+
childSessionDetailParentSessionKey?: string | null;
|
|
27
|
+
childSessionDetailLabel?: string | null;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
type ChatThreadStore = {
|
|
@@ -43,7 +48,12 @@ const initialSnapshot: ChatThreadSnapshot = {
|
|
|
43
48
|
isHistoryLoading: false,
|
|
44
49
|
messages: [],
|
|
45
50
|
isSending: false,
|
|
46
|
-
isAwaitingAssistantOutput: false
|
|
51
|
+
isAwaitingAssistantOutput: false,
|
|
52
|
+
parentSessionKey: null,
|
|
53
|
+
parentSessionLabel: null,
|
|
54
|
+
childSessionDetailSessionKey: null,
|
|
55
|
+
childSessionDetailParentSessionKey: null,
|
|
56
|
+
childSessionDetailLabel: null,
|
|
47
57
|
};
|
|
48
58
|
|
|
49
59
|
export const useChatThreadStore = create<ChatThreadStore>((set) => ({
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { renderHook, waitFor } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import type { NcpAgentClientEndpoint } from "@nextclaw/ncp";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { useHydratedNcpAgent } from "../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-hydrated-ncp-agent.ts";
|
|
4
5
|
|
|
5
6
|
const mocks = vi.hoisted(() => ({
|
|
6
7
|
manager: {
|
|
7
8
|
reset: vi.fn(),
|
|
8
|
-
hydrate: vi.fn()
|
|
9
|
+
hydrate: vi.fn(),
|
|
9
10
|
},
|
|
10
11
|
runtime: {
|
|
11
12
|
snapshot: {
|
|
12
13
|
messages: [],
|
|
13
14
|
streamingMessage: null,
|
|
14
15
|
activeRun: null,
|
|
15
|
-
error: null
|
|
16
|
+
error: null,
|
|
16
17
|
},
|
|
17
18
|
visibleMessages: [],
|
|
18
19
|
activeRunId: null,
|
|
@@ -20,16 +21,19 @@ const mocks = vi.hoisted(() => ({
|
|
|
20
21
|
isSending: false,
|
|
21
22
|
send: vi.fn(),
|
|
22
23
|
abort: vi.fn(),
|
|
23
|
-
streamRun: vi.fn()
|
|
24
|
-
}
|
|
24
|
+
streamRun: vi.fn(),
|
|
25
|
+
},
|
|
25
26
|
}));
|
|
26
27
|
|
|
27
|
-
vi.mock(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
vi.mock(
|
|
29
|
+
"../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-ncp-agent-runtime.js",
|
|
30
|
+
() => ({
|
|
31
|
+
useScopedAgentManager: () => mocks.manager,
|
|
32
|
+
useNcpAgentRuntime: () => mocks.runtime,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
31
35
|
|
|
32
|
-
describe(
|
|
36
|
+
describe("useHydratedNcpAgent", () => {
|
|
33
37
|
beforeEach(() => {
|
|
34
38
|
mocks.manager.reset.mockReset();
|
|
35
39
|
mocks.manager.hydrate.mockReset();
|
|
@@ -38,40 +42,43 @@ describe('useHydratedNcpAgent', () => {
|
|
|
38
42
|
mocks.runtime.streamRun.mockReset();
|
|
39
43
|
});
|
|
40
44
|
|
|
41
|
-
it(
|
|
45
|
+
it("treats a newly selected session as hydrating immediately on rerender", async () => {
|
|
42
46
|
const client = {
|
|
43
47
|
stop: vi.fn().mockResolvedValue(undefined),
|
|
44
|
-
stream: vi.fn().mockResolvedValue(undefined)
|
|
45
|
-
}
|
|
48
|
+
stream: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
} satisfies Pick<NcpAgentClientEndpoint, "stop" | "stream">;
|
|
46
50
|
const loadSeed = vi
|
|
47
51
|
.fn()
|
|
48
|
-
.mockResolvedValueOnce({ messages: [], status:
|
|
49
|
-
.mockResolvedValueOnce({ messages: [], status:
|
|
52
|
+
.mockResolvedValueOnce({ messages: [], status: "idle" })
|
|
53
|
+
.mockResolvedValueOnce({ messages: [], status: "idle" });
|
|
50
54
|
|
|
51
55
|
const { result, rerender } = renderHook(
|
|
52
56
|
({ sessionId }: { sessionId: string }) =>
|
|
53
57
|
useHydratedNcpAgent({
|
|
54
58
|
sessionId,
|
|
55
|
-
client,
|
|
56
|
-
loadSeed
|
|
59
|
+
client: client as never,
|
|
60
|
+
loadSeed,
|
|
57
61
|
}),
|
|
58
62
|
{
|
|
59
63
|
initialProps: {
|
|
60
|
-
sessionId:
|
|
61
|
-
}
|
|
62
|
-
}
|
|
64
|
+
sessionId: "session-a",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
63
67
|
);
|
|
64
68
|
|
|
65
69
|
await waitFor(() => {
|
|
66
70
|
expect(result.current.isHydrating).toBe(false);
|
|
67
71
|
});
|
|
72
|
+
expect(client.stream).toHaveBeenCalledWith({ sessionId: "session-a" });
|
|
68
73
|
|
|
69
|
-
rerender({ sessionId:
|
|
74
|
+
rerender({ sessionId: "session-b" });
|
|
70
75
|
|
|
71
76
|
expect(result.current.isHydrating).toBe(true);
|
|
72
77
|
|
|
73
78
|
await waitFor(() => {
|
|
74
79
|
expect(result.current.isHydrating).toBe(false);
|
|
75
80
|
});
|
|
81
|
+
expect(client.stream).toHaveBeenCalledWith({ sessionId: "session-b" });
|
|
82
|
+
expect(client.stream).toHaveBeenCalledTimes(2);
|
|
76
83
|
});
|
|
77
84
|
});
|