@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/{ChannelsList-Zeys_w43.js → ChannelsList-DVDu1xvz.js} +1 -1
  3. package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
  4. package/dist/assets/{MarketplacePage-Cd4faegU.js → MarketplacePage-Buo9HrOz.js} +1 -1
  5. package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
  6. package/dist/assets/{McpMarketplacePage-C09Ngs7O.js → McpMarketplacePage-JnkYwK7p.js} +1 -1
  7. package/dist/assets/{ModelConfig-DJgdcgvQ.js → ModelConfig-BYRhgp0c.js} +1 -1
  8. package/dist/assets/{ProvidersList-w0rVFIBf.js → ProvidersList-DmLyyHvX.js} +1 -1
  9. package/dist/assets/{RemoteAccessPage-BJ_ckkOV.js → RemoteAccessPage-CDSSvH7Z.js} +1 -1
  10. package/dist/assets/{RuntimeConfig-Cmn2xPQO.js → RuntimeConfig-v7a7Fe3x.js} +1 -1
  11. package/dist/assets/{SearchConfig-BT13qpR_.js → SearchConfig-D5f1EkLE.js} +1 -1
  12. package/dist/assets/{SecretsConfig-CvqEVn0B.js → SecretsConfig-D61IKcYt.js} +1 -1
  13. package/dist/assets/{SessionsConfig-DHHcYznk.js → SessionsConfig-BRIxVTEv.js} +2 -2
  14. package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
  15. package/dist/assets/{index-C6d0xmtm.js → index-BuwbBgmT.js} +2 -2
  16. package/dist/assets/index-bZ8cqQIS.css +1 -0
  17. package/dist/assets/{security-config-T5zpg16O.js → security-config-DbUyWcQz.js} +1 -1
  18. package/dist/assets/{useConfirmDialog-Bs5Ll17m.js → useConfirmDialog-COwYXDKm.js} +1 -1
  19. package/dist/index.html +2 -2
  20. package/package.json +6 -6
  21. package/src/api/types.ts +4 -0
  22. package/src/components/chat/ChatConversationPanel.test.tsx +10 -2
  23. package/src/components/chat/ChatConversationPanel.tsx +114 -77
  24. package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -5
  25. package/src/components/chat/adapters/chat-message.adapter.test.ts +83 -9
  26. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
  27. package/src/components/chat/chat-child-session-panel.tsx +100 -0
  28. package/src/components/chat/chat-page-runtime.test.ts +1 -0
  29. package/src/components/chat/chat-session-display.test.ts +1 -0
  30. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  31. package/src/components/chat/ncp/NcpChatPage.tsx +179 -114
  32. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
  33. package/src/components/chat/ncp/ncp-session-adapter.test.ts +21 -0
  34. package/src/components/chat/ncp/ncp-session-adapter.ts +31 -0
  35. package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
  36. package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
  37. package/src/components/chat/stores/chat-thread.store.ts +11 -1
  38. package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
  39. package/dist/assets/ChatPage-DWOU_8P6.js +0 -43
  40. package/dist/assets/MarketplacePage-BfaTTqN6.js +0 -1
  41. package/dist/assets/chat-session-display-VW6ZMvZP.js +0 -1
  42. package/dist/assets/index-BlH4-cBw.css +0 -1
  43. 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 '@testing-library/react';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { useHydratedNcpAgent } from '../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-hydrated-ncp-agent.ts';
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('../../../../ncp-packages/nextclaw-ncp-react/src/hooks/use-ncp-agent-runtime.js', () => ({
28
- useScopedAgentManager: () => mocks.manager,
29
- useNcpAgentRuntime: () => mocks.runtime
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('useHydratedNcpAgent', () => {
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('treats a newly selected session as hydrating immediately on rerender', async () => {
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
- } as never;
48
+ stream: vi.fn().mockResolvedValue(undefined),
49
+ } satisfies Pick<NcpAgentClientEndpoint, "stop" | "stream">;
46
50
  const loadSeed = vi
47
51
  .fn()
48
- .mockResolvedValueOnce({ messages: [], status: 'idle' })
49
- .mockResolvedValueOnce({ messages: [], status: 'idle' });
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: 'session-a'
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: 'session-b' });
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
  });