@nextclaw/ui 0.11.21 → 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 (129) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/{ChannelsList-ByHWHkQS.js → ChannelsList-DVDu1xvz.js} +6 -6
  3. package/dist/assets/ChatPage-Z9tRzm_n.js +43 -0
  4. package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
  5. package/dist/assets/{DocBrowser-3y_NHZ71.js → DocBrowser-BmtBLFU0.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-CVJuwCcw.js → DocBrowserContext-YIKkPb76.js} +1 -1
  7. package/dist/assets/{LogoBadge-D8fyilO-.js → LogoBadge-F7ZWdxLT.js} +1 -1
  8. package/dist/assets/{MarketplacePage-CmhsZXr1.js → MarketplacePage-Buo9HrOz.js} +2 -2
  9. package/dist/assets/MarketplacePage-D6rVQEQR.js +1 -0
  10. package/dist/assets/{McpMarketplacePage-C7PkCYbp.js → McpMarketplacePage-JnkYwK7p.js} +2 -2
  11. package/dist/assets/ModelConfig-BYRhgp0c.js +1 -0
  12. package/dist/assets/ProvidersList-DmLyyHvX.js +1 -0
  13. package/dist/assets/RemoteAccessPage-CDSSvH7Z.js +1 -0
  14. package/dist/assets/RuntimeConfig-v7a7Fe3x.js +1 -0
  15. package/dist/assets/{SearchConfig-Dm7r2yfp.js → SearchConfig-D5f1EkLE.js} +1 -1
  16. package/dist/assets/{SecretsConfig-BBP_mbQh.js → SecretsConfig-D61IKcYt.js} +2 -2
  17. package/dist/assets/{SessionsConfig-6wNJloZN.js → SessionsConfig-BRIxVTEv.js} +2 -2
  18. package/dist/assets/{book-open-B26jGBjY.js → book-open-CXoF5nQC.js} +1 -1
  19. package/dist/assets/chat-session-display-D0WpnuRZ.js +1 -0
  20. package/dist/assets/{chunk-JZWAC4HX-B-4B29RN.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
  21. package/dist/assets/{config-BaC29Qf-.js → config-DJswxxE8.js} +1 -1
  22. package/dist/assets/{createLucideIcon-DiFAvXmK.js → createLucideIcon-CjGHOWb6.js} +1 -1
  23. package/dist/assets/{dist-pCfWPG1A.js → dist-Cl2QB-2y.js} +1 -1
  24. package/dist/assets/{dist-kW_O3kyZ.js → dist-nqTTbVdA.js} +1 -1
  25. package/dist/assets/{external-link-D5-p-Gmm.js → external-link-tIO7zING.js} +1 -1
  26. package/dist/assets/{hash-BlwrSV0q.js → hash-JWUyl1pT.js} +1 -1
  27. package/dist/assets/i18n-CDHMXlRZ.js +1 -0
  28. package/dist/assets/{index-DvKS3L9j.js → index-BuwbBgmT.js} +3 -3
  29. package/dist/assets/index-bZ8cqQIS.css +1 -0
  30. package/dist/assets/{label-RyXfZqkP.js → label-BIpeNu4r.js} +1 -1
  31. package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
  32. package/dist/assets/{logos-Bpl8QTgI.js → logos-DThdM9lk.js} +1 -1
  33. package/dist/assets/{page-layout--S0YBU0W.js → page-layout-D3Xo605Z.js} +1 -1
  34. package/dist/assets/plus-PHf8q-Ct.js +1 -0
  35. package/dist/assets/{popover-BEjfbEwy.js → popover-BJRUGA_H.js} +1 -1
  36. package/dist/assets/provider-models-bz5y28rq.js +1 -0
  37. package/dist/assets/{react-BuSP2-8B.js → react-7ZHqQtEV.js} +1 -1
  38. package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
  39. package/dist/assets/{save-DPPPpD_c.js → save-DJM5RRWW.js} +1 -1
  40. package/dist/assets/search-C91yH_6y.js +1 -0
  41. package/dist/assets/{security-config-6t78Ph-I.js → security-config-DbUyWcQz.js} +1 -1
  42. package/dist/assets/{select-CT50pzod.js → select-DSkTc61S.js} +1 -1
  43. package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
  44. package/dist/assets/{status-dot-BbBqRHfh.js → status-dot-LNBlDu3q.js} +1 -1
  45. package/dist/assets/{switch-D3l6AcCk.js → switch-Bo-Y46HZ.js} +1 -1
  46. package/dist/assets/tabs-custom-DXv507_2.js +1 -0
  47. package/dist/assets/{trash-2-B2_AGVE3.js → trash-2-DFZmW6Gg.js} +1 -1
  48. package/dist/assets/useConfirmDialog-COwYXDKm.js +1 -0
  49. package/dist/assets/{useMutation-BzCrO8j-.js → useMutation-DrZrOgVL.js} +1 -1
  50. package/dist/assets/x-D7Q1yqSF.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +6 -6
  53. package/src/api/ncp-session.test.ts +37 -0
  54. package/src/api/ncp-session.ts +29 -1
  55. package/src/api/server-path.ts +23 -0
  56. package/src/api/types.ts +45 -0
  57. package/src/components/chat/ChatConversationPanel.test.tsx +53 -9
  58. package/src/components/chat/ChatConversationPanel.tsx +122 -79
  59. package/src/components/chat/ChatSidebar.test.tsx +2 -2
  60. package/src/components/chat/ChatSidebar.tsx +2 -2
  61. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
  62. package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
  63. package/src/components/chat/adapters/chat-message-part.adapter.ts +26 -14
  64. package/src/components/chat/adapters/chat-message.adapter.test.ts +159 -13
  65. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +191 -0
  66. package/src/components/chat/adapters/{chat-message.file-operation-card.ts → file-operation/card.ts} +74 -181
  67. package/src/components/chat/adapters/{chat-message.file-operation-diff.ts → file-operation/diff.ts} +178 -188
  68. package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
  69. package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
  70. package/src/components/chat/chat-child-session-panel.tsx +100 -0
  71. package/src/components/chat/chat-composer-state.ts +3 -3
  72. package/src/components/chat/chat-page-runtime.test.ts +1 -0
  73. package/src/components/chat/chat-session-display.test.ts +22 -0
  74. package/src/components/chat/chat-session-display.ts +6 -1
  75. package/src/components/chat/containers/chat-input-bar.container.tsx +21 -24
  76. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  77. package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
  78. package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
  79. package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
  80. package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
  81. package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
  82. package/src/components/chat/ncp/NcpChatPage.tsx +219 -116
  83. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
  84. package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
  85. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +49 -0
  86. package/src/components/chat/ncp/ncp-session-adapter.test.ts +24 -0
  87. package/src/components/chat/ncp/ncp-session-adapter.ts +47 -0
  88. package/src/components/chat/ncp/use-ncp-session-list-view.ts +10 -1
  89. package/src/components/chat/presenter/chat-presenter-context.tsx +4 -1
  90. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
  91. package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
  92. package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
  93. package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
  94. package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
  95. package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
  96. package/src/components/chat/stores/chat-input.store.ts +6 -3
  97. package/src/components/chat/stores/chat-thread.store.ts +17 -3
  98. package/src/components/chat/useHydratedNcpAgent.test.tsx +30 -23
  99. package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
  100. package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
  101. package/src/hooks/server-path/use-server-path-browse.ts +19 -0
  102. package/src/hooks/useConfig.ts +26 -1
  103. package/src/lib/i18n/i18n-language-owner.ts +94 -0
  104. package/src/lib/i18n/i18n.path-picker.ts +12 -0
  105. package/src/lib/i18n.chat.ts +23 -0
  106. package/src/lib/i18n.ts +21 -84
  107. package/src/lib/session-project/session-project.utils.ts +30 -0
  108. package/dist/assets/ChatPage-FdT3pDnw.js +0 -42
  109. package/dist/assets/DocBrowser-CMdPdbZj.js +0 -1
  110. package/dist/assets/MarketplacePage-9oKmxN2n.js +0 -1
  111. package/dist/assets/ModelConfig-DmCY6jWM.js +0 -1
  112. package/dist/assets/ProvidersList-ClT-34aX.js +0 -1
  113. package/dist/assets/RemoteAccessPage-B6hUZl1O.js +0 -1
  114. package/dist/assets/RuntimeConfig-C5aqliGk.js +0 -1
  115. package/dist/assets/chat-session-display-Bjmn4aIZ.js +0 -1
  116. package/dist/assets/i18n-CSytxMFI.js +0 -1
  117. package/dist/assets/index-CUy6doWo.css +0 -1
  118. package/dist/assets/loader-circle-B2J777gj.js +0 -1
  119. package/dist/assets/plus-CM9XJ0Tf.js +0 -1
  120. package/dist/assets/provider-models-C8JQUd1E.js +0 -1
  121. package/dist/assets/search-Ctaw34Kp.js +0 -1
  122. package/dist/assets/skeleton-Bycyb0zU.js +0 -1
  123. package/dist/assets/tabs-custom-TZQ5WPWP.js +0 -1
  124. package/dist/assets/useConfirmDialog-BDpdjfIO.js +0 -1
  125. package/dist/assets/x-CHOBE-63.js +0 -1
  126. package/src/components/chat/adapters/chat-message.subagent-tool-card.ts +0 -154
  127. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
  128. /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
  129. /package/dist/assets/{marketplace-localization-CXeGRf6E.js → marketplace-localization-CxSTG9wr.js} +0 -0
@@ -1,34 +1,59 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { NcpHttpAgentClientEndpoint } from '@nextclaw/ncp-http-agent-client';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type MutableRefObject,
8
+ } from "react";
9
+ import { NcpHttpAgentClientEndpoint } from "@nextclaw/ncp-http-agent-client";
3
10
  import {
4
11
  buildNcpRequestEnvelope,
5
12
  useHydratedNcpAgent,
6
- type NcpConversationSeed
7
- } from '@nextclaw/ncp-react';
8
- import { useLocation, useNavigate, useParams } from 'react-router-dom';
9
- import { API_BASE } from '@/api/api-base';
10
- import { fetchNcpSessionMessages } from '@/api/ncp-session';
11
- import { ChatPageLayout, type ChatPageProps, useChatSessionSync } from '@/components/chat/chat-page-shell';
12
- import { sessionDisplayName } from '@/components/chat/chat-session-display';
13
- import { buildInlineSkillTokensFromComposer, CHAT_UI_INLINE_TOKENS_METADATA_KEY } from '@/components/chat/chat-inline-token.utils';
14
- import { createNcpAppClientFetch } from '@/components/chat/ncp/ncp-app-client-fetch';
15
- import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
16
- import { useNcpChatPageData } from '@/components/chat/ncp/ncp-chat-page-data';
17
- import { NcpChatPresenter } from '@/components/chat/ncp/ncp-chat.presenter';
18
- import { createNcpSessionId } from '@/components/chat/ncp/ncp-session-adapter';
19
- import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
20
- import type { ResumeRunParams } from '@/components/chat/chat-stream/types';
21
- import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
22
- import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
23
- import { resolveSessionTypeLabel } from '@/components/chat/useChatSessionTypeState';
24
- import { useConfirmDialog } from '@/hooks/useConfirmDialog';
25
- import { normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
26
- import { appClient } from '@/transport';
13
+ type NcpConversationSeed,
14
+ type NcpConversationSeedLoader,
15
+ } from "@nextclaw/ncp-react";
16
+ import { useLocation, useNavigate, useParams } from "react-router-dom";
17
+ import { API_BASE } from "@/api/api-base";
18
+ import { fetchNcpSessionMessages } from "@/api/ncp-session";
19
+ import {
20
+ ChatPageLayout,
21
+ type ChatPageProps,
22
+ useChatSessionSync,
23
+ } from "@/components/chat/chat-page-shell";
24
+ import { sessionDisplayName } from "@/components/chat/chat-session-display";
25
+ import {
26
+ buildInlineSkillTokensFromComposer,
27
+ CHAT_UI_INLINE_TOKENS_METADATA_KEY,
28
+ } from "@/components/chat/chat-inline-token.utils";
29
+ import { createNcpAppClientFetch } from "@/components/chat/ncp/ncp-app-client-fetch";
30
+ import {
31
+ parseSessionKeyFromRoute,
32
+ resolveAgentIdFromSessionKey,
33
+ } from "@/components/chat/chat-session-route";
34
+ import { useNcpChatPageData } from "@/components/chat/ncp/ncp-chat-page-data";
35
+ import { NcpChatPresenter } from "@/components/chat/ncp/ncp-chat.presenter";
36
+ import {
37
+ adaptNcpSessionSummary,
38
+ createNcpSessionId,
39
+ } from "@/components/chat/ncp/ncp-session-adapter";
40
+ import { ChatPresenterProvider } from "@/components/chat/presenter/chat-presenter-context";
41
+ import type { ResumeRunParams } from "@/components/chat/chat-stream/types";
42
+ import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
43
+ import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
44
+ import { resolveSessionTypeLabel } from "@/components/chat/useChatSessionTypeState";
45
+ import { useConfirmDialog } from "@/hooks/useConfirmDialog";
46
+ import { normalizeRequestedSkills } from "@/lib/chat-runtime-utils";
47
+ import {
48
+ getSessionProjectName,
49
+ normalizeSessionProjectRootValue,
50
+ } from "@/lib/session-project/session-project.utils";
27
51
 
28
- function buildNcpSendMetadata(payload: {
52
+ export function buildNcpSendMetadata(payload: {
29
53
  model?: string;
30
54
  thinkingLevel?: string;
31
55
  sessionType?: string;
56
+ projectRoot?: string | null;
32
57
  requestedSkills?: string[];
33
58
  composerNodes?: Parameters<typeof buildInlineSkillTokensFromComposer>[0];
34
59
  }): Record<string, unknown> {
@@ -44,9 +69,13 @@ function buildNcpSendMetadata(payload: {
44
69
  if (payload.sessionType?.trim()) {
45
70
  metadata.session_type = payload.sessionType.trim();
46
71
  }
72
+ const projectRoot = normalizeSessionProjectRootValue(payload.projectRoot);
73
+ if (projectRoot) {
74
+ metadata.project_root = projectRoot;
75
+ }
47
76
  const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
48
77
  if (requestedSkills.length > 0) {
49
- metadata.requested_skills = requestedSkills;
78
+ metadata.requested_skill_refs = requestedSkills;
50
79
  }
51
80
  const inlineSkillTokens = payload.composerNodes
52
81
  ? buildInlineSkillTokensFromComposer(payload.composerNodes)
@@ -61,30 +90,91 @@ function isMissingNcpSessionError(error: unknown): boolean {
61
90
  if (!(error instanceof Error)) {
62
91
  return false;
63
92
  }
64
- return error.message.includes('ncp session not found:');
93
+ return error.message.includes("ncp session not found:");
94
+ }
95
+
96
+ type NcpSeedSessionSummary = {
97
+ sessionId: string;
98
+ status?: string;
99
+ };
100
+
101
+ function useNcpConversationSeedLoader<T extends NcpSeedSessionSummary>(
102
+ sessionSummariesRef: MutableRefObject<readonly T[]>,
103
+ ): NcpConversationSeedLoader {
104
+ return useCallback(
105
+ async (
106
+ sessionId: string,
107
+ signal: AbortSignal,
108
+ ): Promise<NcpConversationSeed> => {
109
+ signal.throwIfAborted();
110
+ let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null =
111
+ null;
112
+ try {
113
+ history = await fetchNcpSessionMessages(sessionId, 300);
114
+ } catch (error) {
115
+ if (!isMissingNcpSessionError(error)) {
116
+ throw error;
117
+ }
118
+ }
119
+ signal.throwIfAborted();
120
+
121
+ const sessionSummary =
122
+ sessionSummariesRef.current.find(
123
+ (item) => item.sessionId === sessionId,
124
+ ) ?? null;
125
+ return {
126
+ messages: history?.messages ?? [],
127
+ status: sessionSummary?.status === "running" ? "running" : "idle",
128
+ };
129
+ },
130
+ [sessionSummariesRef],
131
+ );
65
132
  }
66
133
 
67
134
  export function NcpChatPage({ view }: ChatPageProps) {
68
135
  const [presenter] = useState(() => new NcpChatPresenter());
69
- const [draftSessionId, setDraftSessionId] = useState(() => createNcpSessionId());
136
+ const [draftSessionId, setDraftSessionId] = useState(() =>
137
+ createNcpSessionId(),
138
+ );
70
139
  const query = useChatSessionListStore((state) => state.snapshot.query);
71
- const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
72
- const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
73
- const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
74
- const currentSelectedModel = useChatInputStore((state) => state.snapshot.selectedModel);
140
+ const selectedSessionKey = useChatSessionListStore(
141
+ (state) => state.snapshot.selectedSessionKey,
142
+ );
143
+ const selectedAgentId = useChatSessionListStore(
144
+ (state) => state.snapshot.selectedAgentId,
145
+ );
146
+ const pendingSessionType = useChatInputStore(
147
+ (state) => state.snapshot.pendingSessionType,
148
+ );
149
+ const pendingProjectRoot = useChatInputStore(
150
+ (state) => state.snapshot.pendingProjectRoot,
151
+ );
152
+ const pendingProjectRootSessionKey = useChatInputStore(
153
+ (state) => state.snapshot.pendingProjectRootSessionKey,
154
+ );
155
+ const currentSelectedModel = useChatInputStore(
156
+ (state) => state.snapshot.selectedModel,
157
+ );
75
158
  const { confirm, ConfirmDialog } = useConfirmDialog();
76
159
  const location = useLocation();
77
160
  const navigate = useNavigate();
78
- const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
161
+ const { sessionId: routeSessionIdParam } = useParams<{
162
+ sessionId?: string;
163
+ }>();
79
164
  const threadRef = useRef<HTMLDivElement | null>(null);
80
165
  const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
81
- const sessionStreamAttachInFlightRef = useRef(false);
82
166
  const routeSessionKey = useMemo(
83
167
  () => parseSessionKeyFromRoute(routeSessionIdParam),
84
- [routeSessionIdParam]
168
+ [routeSessionIdParam],
85
169
  );
170
+ const sessionKey = selectedSessionKey ?? draftSessionId;
171
+ const hasSessionProjectRootOverride =
172
+ pendingProjectRootSessionKey === sessionKey;
173
+ const sessionProjectRootOverride = hasSessionProjectRootOverride
174
+ ? pendingProjectRoot
175
+ : undefined;
86
176
  const {
87
- installedSkillsQuery,
177
+ sessionSkillsQuery,
88
178
  isProviderStateResolved,
89
179
  modelOptions,
90
180
  sessionSummaries,
@@ -95,18 +185,19 @@ export function NcpChatPage({ view }: ChatPageProps) {
95
185
  selectedSessionType,
96
186
  canEditSessionType,
97
187
  sessionTypeUnavailable,
98
- sessionTypeUnavailableMessage
188
+ sessionTypeUnavailableMessage,
99
189
  } = useNcpChatPageData({
100
190
  query,
101
- selectedSessionKey,
191
+ sessionKey,
192
+ projectRootOverride: sessionProjectRootOverride,
102
193
  currentSelectedModel,
103
194
  pendingSessionType,
104
195
  setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
105
196
  setSelectedModel: presenter.chatInputManager.setSelectedModel,
106
- setSelectedThinkingLevel: presenter.chatInputManager.setSelectedThinkingLevel
197
+ setSelectedThinkingLevel:
198
+ presenter.chatInputManager.setSelectedThinkingLevel,
107
199
  });
108
200
 
109
- const activeSessionId = selectedSessionKey ?? draftSessionId;
110
201
  const sessionSummariesRef = useRef(sessionSummaries);
111
202
  useEffect(() => {
112
203
  sessionSummariesRef.current = sessionSummaries;
@@ -115,35 +206,18 @@ export function NcpChatPage({ view }: ChatPageProps) {
115
206
  const [ncpClient] = useState(
116
207
  () =>
117
208
  new NcpHttpAgentClientEndpoint({
118
- baseUrl: API_BASE,
119
- basePath: '/api/ncp/agent',
120
- fetchImpl: createNcpAppClientFetch()
121
- })
209
+ baseUrl: API_BASE,
210
+ basePath: "/api/ncp/agent",
211
+ fetchImpl: createNcpAppClientFetch(),
212
+ }),
122
213
  );
123
214
 
124
- const loadSeed = useCallback(async (sessionId: string, signal: AbortSignal): Promise<NcpConversationSeed> => {
125
- signal.throwIfAborted();
126
- let history: Awaited<ReturnType<typeof fetchNcpSessionMessages>> | null = null;
127
- try {
128
- history = await fetchNcpSessionMessages(sessionId, 300);
129
- } catch (error) {
130
- if (!isMissingNcpSessionError(error)) {
131
- throw error;
132
- }
133
- }
134
- signal.throwIfAborted();
135
-
136
- const sessionSummary = sessionSummariesRef.current.find((item) => item.sessionId === sessionId) ?? null;
137
- return {
138
- messages: history?.messages ?? [],
139
- status: sessionSummary?.status === 'running' ? 'running' : 'idle'
140
- };
141
- }, []);
215
+ const loadSeed = useNcpConversationSeedLoader(sessionSummariesRef);
142
216
 
143
217
  const agent = useHydratedNcpAgent({
144
- sessionId: activeSessionId,
218
+ sessionId: sessionKey,
145
219
  client: ncpClient,
146
- loadSeed
220
+ loadSeed,
147
221
  });
148
222
 
149
223
  useEffect(() => {
@@ -158,60 +232,45 @@ export function NcpChatPage({ view }: ChatPageProps) {
158
232
  }
159
233
  }, [presenter, selectedSessionKey]);
160
234
 
235
+ const effectiveSessionProjectRoot = hasSessionProjectRootOverride
236
+ ? pendingProjectRoot
237
+ : (selectedSession?.projectRoot ?? null);
238
+ const effectiveSessionProjectName = hasSessionProjectRootOverride
239
+ ? getSessionProjectName(effectiveSessionProjectRoot)
240
+ : (selectedSession?.projectName ??
241
+ getSessionProjectName(effectiveSessionProjectRoot));
242
+ const parentSessionId = selectedSession?.parentSessionId ?? null;
243
+
161
244
  const isSending = agent.isSending || agent.isRunning;
162
245
  const isAwaitingAssistantOutput = agent.isRunning;
163
246
  const canStopCurrentRun = agent.isRunning;
164
- const stopDisabledReason = agent.isRunning ? null : '__preparing__';
165
- const lastSendError = agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
166
-
167
- useEffect(() => {
168
- const attachRealtimeSessionStream = () => {
169
- if (sessionStreamAttachInFlightRef.current) {
170
- return;
171
- }
172
- if (agent.isHydrating || agent.isRunning || agent.isSending) {
173
- return;
174
- }
175
-
176
- sessionStreamAttachInFlightRef.current = true;
177
- void ncpClient
178
- .stream({ sessionId: activeSessionId })
179
- .catch(() => undefined)
180
- .finally(() => {
181
- sessionStreamAttachInFlightRef.current = false;
182
- });
183
- };
184
-
185
- return appClient.subscribe((event) => {
186
- if (
187
- event.type === 'session.run-status' &&
188
- event.payload.sessionKey === activeSessionId &&
189
- event.payload.status === 'running'
190
- ) {
191
- attachRealtimeSessionStream();
192
- }
193
- });
194
- }, [activeSessionId, agent.isHydrating, agent.isRunning, agent.isSending, ncpClient]);
247
+ const stopDisabledReason = agent.isRunning ? null : "__preparing__";
248
+ const lastSendError =
249
+ agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
195
250
 
196
251
  useEffect(() => {
197
252
  presenter.chatStreamActionsManager.bind({
198
253
  sendMessage: async (payload) => {
199
- if (payload.sessionKey !== activeSessionId) {
254
+ if (payload.sessionKey !== sessionKey) {
200
255
  return;
201
256
  }
202
257
  const metadata = buildNcpSendMetadata({
203
258
  model: payload.model,
204
259
  thinkingLevel: payload.thinkingLevel,
205
260
  sessionType: payload.sessionType,
261
+ projectRoot:
262
+ payload.sessionKey === pendingProjectRootSessionKey
263
+ ? pendingProjectRoot
264
+ : (selectedSession?.projectRoot ?? null),
206
265
  requestedSkills: payload.requestedSkills,
207
- composerNodes: payload.composerNodes
266
+ composerNodes: payload.composerNodes,
208
267
  });
209
268
  const envelope = buildNcpRequestEnvelope({
210
269
  sessionId: payload.sessionKey,
211
270
  text: payload.message,
212
271
  attachments: payload.attachments,
213
272
  parts: payload.parts,
214
- metadata
273
+ metadata,
215
274
  });
216
275
  if (!envelope) {
217
276
  return;
@@ -223,11 +282,13 @@ export function NcpChatPage({ view }: ChatPageProps) {
223
282
  if (payload.composerNodes && payload.composerNodes.length > 0) {
224
283
  presenter.chatInputManager.restoreComposerState?.(
225
284
  payload.composerNodes,
226
- payload.attachments ?? []
285
+ payload.attachments ?? [],
227
286
  );
228
287
  } else {
229
288
  presenter.chatInputManager.setDraft((currentDraft) =>
230
- currentDraft.trim().length === 0 ? payload.message : currentDraft
289
+ currentDraft.trim().length === 0
290
+ ? payload.message
291
+ : currentDraft,
231
292
  );
232
293
  }
233
294
  }
@@ -238,7 +299,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
238
299
  await agent.abort();
239
300
  },
240
301
  resumeRun: async (run: ResumeRunParams) => {
241
- if (run.sessionKey !== activeSessionId) {
302
+ if (run.sessionKey !== sessionKey) {
242
303
  return;
243
304
  }
244
305
  await agent.streamRun();
@@ -246,36 +307,70 @@ export function NcpChatPage({ view }: ChatPageProps) {
246
307
  resetStreamState: () => {
247
308
  selectedSessionKeyRef.current = null;
248
309
  },
249
- applyHistoryMessages: () => {}
310
+ applyHistoryMessages: () => {},
250
311
  });
251
- }, [activeSessionId, agent, presenter]);
312
+ }, [
313
+ agent,
314
+ pendingProjectRoot,
315
+ pendingProjectRootSessionKey,
316
+ presenter,
317
+ selectedSession?.projectRoot,
318
+ sessionKey,
319
+ ]);
320
+
321
+ useEffect(() => {
322
+ if (
323
+ !selectedSession ||
324
+ pendingProjectRootSessionKey !== selectedSession.key ||
325
+ (selectedSession.projectRoot ?? null) !== pendingProjectRoot
326
+ ) {
327
+ return;
328
+ }
329
+ useChatInputStore.getState().setSnapshot({
330
+ pendingProjectRoot: null,
331
+ pendingProjectRootSessionKey: null,
332
+ });
333
+ }, [pendingProjectRoot, pendingProjectRootSessionKey, selectedSession]);
252
334
 
253
335
  useChatSessionSync({
254
336
  view,
255
337
  routeSessionKey,
256
338
  selectedSessionKey,
257
339
  selectedAgentId,
258
- setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
340
+ setSelectedSessionKey:
341
+ presenter.chatSessionListManager.setSelectedSessionKey,
259
342
  setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
260
343
  selectedSessionKeyRef,
261
344
  resetStreamState: presenter.chatStreamActionsManager.resetStreamState,
262
- resolveAgentIdFromSessionKey
345
+ resolveAgentIdFromSessionKey,
263
346
  });
264
347
 
265
348
  useEffect(() => {
266
349
  presenter.chatUiManager.syncState({
267
- pathname: location.pathname
350
+ pathname: location.pathname,
268
351
  });
269
352
  presenter.chatUiManager.bindActions({
270
353
  navigate,
271
- confirm
354
+ confirm,
272
355
  });
273
356
  }, [confirm, location.pathname, navigate, presenter]);
274
357
 
275
- const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
358
+ const currentSessionDisplayName = selectedSession
359
+ ? sessionDisplayName(selectedSession)
360
+ : undefined;
361
+ const parentSession = useMemo(() => {
362
+ if (!parentSessionId) {
363
+ return null;
364
+ }
365
+ const parentSummary =
366
+ sessionSummaries.find(
367
+ (summary) => summary.sessionId === parentSessionId,
368
+ ) ?? null;
369
+ return parentSummary ? adaptNcpSessionSummary(parentSummary) : null;
370
+ }, [parentSessionId, sessionSummaries]);
276
371
  const currentSessionTypeLabel =
277
- sessionTypeOptions.find((option) => option.value === selectedSessionType)?.label ??
278
- resolveSessionTypeLabel(selectedSessionType);
372
+ sessionTypeOptions.find((option) => option.value === selectedSessionType)
373
+ ?.label ?? resolveSessionTypeLabel(selectedSessionType);
279
374
 
280
375
  useEffect(() => {
281
376
  presenter.chatInputManager.syncSnapshot({
@@ -293,7 +388,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
293
388
  canEditSessionType,
294
389
  sessionTypeUnavailable,
295
390
  skillRecords,
296
- isSkillsLoading: installedSkillsQuery.isLoading
391
+ isSkillsLoading: sessionSkillsQuery.isLoading,
297
392
  });
298
393
  presenter.chatThreadManager.syncSnapshot({
299
394
  isProviderStateResolved,
@@ -301,14 +396,20 @@ export function NcpChatPage({ view }: ChatPageProps) {
301
396
  sessionTypeUnavailable,
302
397
  sessionTypeUnavailableMessage,
303
398
  sessionTypeLabel: currentSessionTypeLabel,
304
- selectedSessionKey,
399
+ sessionKey,
305
400
  sessionDisplayName: currentSessionDisplayName,
401
+ sessionProjectRoot: effectiveSessionProjectRoot,
402
+ sessionProjectName: effectiveSessionProjectName,
306
403
  canDeleteSession: Boolean(selectedSession),
307
404
  threadRef,
308
405
  isHistoryLoading: agent.isHydrating,
309
406
  messages: agent.visibleMessages,
310
407
  isSending,
311
- isAwaitingAssistantOutput
408
+ isAwaitingAssistantOutput,
409
+ parentSessionKey: parentSession?.key ?? null,
410
+ parentSessionLabel: parentSession
411
+ ? sessionDisplayName(parentSession)
412
+ : null,
312
413
  });
313
414
  }, [
314
415
  agent.isHydrating,
@@ -317,16 +418,18 @@ export function NcpChatPage({ view }: ChatPageProps) {
317
418
  currentSessionDisplayName,
318
419
  currentSessionTypeLabel,
319
420
  defaultSessionType,
320
- installedSkillsQuery.isLoading,
421
+ sessionSkillsQuery.isLoading,
321
422
  isAwaitingAssistantOutput,
322
423
  isProviderStateResolved,
323
424
  isSending,
324
425
  lastSendError,
325
- modelOptions.length,
326
426
  modelOptions,
427
+ parentSession,
327
428
  presenter,
429
+ effectiveSessionProjectName,
430
+ effectiveSessionProjectRoot,
328
431
  selectedSession,
329
- selectedSessionKey,
432
+ sessionKey,
330
433
  selectedSessionType,
331
434
  sessionTypeOptions,
332
435
  sessionTypeUnavailable,
@@ -334,7 +437,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
334
437
  skillRecords,
335
438
  stopDisabledReason,
336
439
  threadRef,
337
- agent.visibleMessages
440
+ agent.visibleMessages,
338
441
  ]);
339
442
 
340
443
  return (
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import { buildNcpSendMetadata } from '@/components/chat/ncp/NcpChatPage';
2
3
  import { filterModelOptionsBySessionType } from '@/components/chat/ncp/ncp-chat-page-data';
3
4
  import type { ChatModelOption } from '@/components/chat/chat-input.types';
4
5
 
@@ -42,3 +43,35 @@ describe('filterModelOptionsBySessionType', () => {
42
43
  ).toEqual(modelOptions);
43
44
  });
44
45
  });
46
+
47
+ describe('buildNcpSendMetadata', () => {
48
+ it('includes the project root in the first-message metadata when present', () => {
49
+ expect(
50
+ buildNcpSendMetadata({
51
+ sessionType: 'codex',
52
+ projectRoot: ' /tmp/project-alpha ',
53
+ }),
54
+ ).toMatchObject({
55
+ session_type: 'codex',
56
+ project_root: '/tmp/project-alpha',
57
+ });
58
+ });
59
+
60
+ it('omits project_root when the input is blank', () => {
61
+ expect(
62
+ buildNcpSendMetadata({
63
+ projectRoot: ' ',
64
+ }),
65
+ ).not.toHaveProperty('project_root');
66
+ });
67
+
68
+ it('sends requested skill refs instead of legacy requested skill names', () => {
69
+ expect(
70
+ buildNcpSendMetadata({
71
+ requestedSkills: ['project:/tmp/project-alpha/.agents/skills/review'],
72
+ }),
73
+ ).toMatchObject({
74
+ requested_skill_refs: ['project:/tmp/project-alpha/.agents/skills/review'],
75
+ });
76
+ });
77
+ });
@@ -14,15 +14,16 @@ import {
14
14
  import {
15
15
  useConfig,
16
16
  useConfigMeta,
17
+ useNcpSessionSkills,
17
18
  useNcpSessions
18
19
  } from '@/hooks/useConfig';
19
20
  import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
20
- import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
21
21
  import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/lib/provider-models';
22
22
 
23
23
  type UseNcpChatPageDataParams = {
24
24
  query: string;
25
- selectedSessionKey: string | null;
25
+ sessionKey: string;
26
+ projectRootOverride?: string | null;
26
27
  currentSelectedModel: string;
27
28
  pendingSessionType: string;
28
29
  setPendingSessionType: Dispatch<SetStateAction<string>>;
@@ -51,7 +52,12 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
51
52
  const configMetaQuery = useConfigMeta();
52
53
  const sessionsQuery = useNcpSessions({ limit: 200 });
53
54
  const sessionTypesQuery = useNcpChatSessionTypes();
54
- const installedSkillsQuery = useMarketplaceInstalled('skill');
55
+ const sessionSkillsQuery = useNcpSessionSkills({
56
+ sessionId: params.sessionKey,
57
+ ...(Object.prototype.hasOwnProperty.call(params, 'projectRootOverride')
58
+ ? { projectRoot: params.projectRootOverride ?? null }
59
+ : {})
60
+ });
55
61
  const isProviderStateResolved =
56
62
  (configQuery.isFetched || configQuery.isSuccess) &&
57
63
  (configMetaQuery.isFetched || configMetaQuery.isSuccess);
@@ -101,16 +107,16 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
101
107
  [allSessions, params.query]
102
108
  );
103
109
  const selectedSession = useMemo(
104
- () => allSessions.find((session) => session.key === params.selectedSessionKey) ?? null,
105
- [allSessions, params.selectedSessionKey]
110
+ () => allSessions.find((session) => session.key === params.sessionKey) ?? null,
111
+ [allSessions, params.sessionKey]
106
112
  );
107
113
  const skillRecords = useMemo(
108
- () => installedSkillsQuery.data?.records ?? [],
109
- [installedSkillsQuery.data?.records]
114
+ () => sessionSkillsQuery.data?.records ?? [],
115
+ [sessionSkillsQuery.data?.records]
110
116
  );
111
117
  const sessionTypeState = useChatSessionTypeState({
112
118
  selectedSession,
113
- selectedSessionKey: params.selectedSessionKey,
119
+ selectedSessionKey: params.sessionKey,
114
120
  pendingSessionType: params.pendingSessionType,
115
121
  setPendingSessionType: params.setPendingSessionType,
116
122
  sessionTypesData: sessionTypesQuery.data
@@ -127,10 +133,10 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
127
133
  () =>
128
134
  resolveRecentSessionPreferredModel({
129
135
  sessions: allSessions,
130
- selectedSessionKey: params.selectedSessionKey,
136
+ selectedSessionKey: params.sessionKey,
131
137
  sessionType: sessionTypeState.selectedSessionType
132
138
  }),
133
- [allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
139
+ [allSessions, params.sessionKey, sessionTypeState.selectedSessionType]
134
140
  );
135
141
  const currentModelOption = useMemo(
136
142
  () => filteredModelOptions.find((option) => option.value === params.currentSelectedModel),
@@ -148,15 +154,15 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
148
154
  () =>
149
155
  resolveRecentSessionPreferredThinking({
150
156
  sessions: allSessions,
151
- selectedSessionKey: params.selectedSessionKey,
157
+ selectedSessionKey: params.sessionKey,
152
158
  sessionType: sessionTypeState.selectedSessionType
153
159
  }),
154
- [allSessions, params.selectedSessionKey, sessionTypeState.selectedSessionType]
160
+ [allSessions, params.sessionKey, sessionTypeState.selectedSessionType]
155
161
  );
156
162
 
157
163
  useSyncSelectedModel({
158
164
  modelOptions: filteredModelOptions,
159
- selectedSessionKey: params.selectedSessionKey,
165
+ selectedSessionKey: params.sessionKey,
160
166
  selectedSessionExists: Boolean(selectedSession),
161
167
  selectedSessionPreferredModel: selectedSession?.preferredModel,
162
168
  fallbackPreferredModel: sessionTypeState.selectedSessionTypeOption?.recommendedModel ?? recentSessionPreferredModel,
@@ -165,7 +171,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
165
171
  });
166
172
  useSyncSelectedThinking({
167
173
  supportedThinkingLevels,
168
- selectedSessionKey: params.selectedSessionKey,
174
+ selectedSessionKey: params.sessionKey,
169
175
  selectedSessionExists: Boolean(selectedSession),
170
176
  selectedSessionPreferredThinking: selectedSession?.preferredThinking ?? null,
171
177
  fallbackPreferredThinking: recentSessionPreferredThinking ?? null,
@@ -178,7 +184,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
178
184
  configMetaQuery,
179
185
  sessionsQuery,
180
186
  sessionTypesQuery,
181
- installedSkillsQuery,
187
+ sessionSkillsQuery,
182
188
  isProviderStateResolved,
183
189
  modelOptions: filteredModelOptions,
184
190
  sessionSummaries,