@nextclaw/ui 0.12.4 → 0.12.6

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 (149) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
  3. package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
  4. package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
  5. package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
  7. package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
  8. package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
  9. package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
  10. package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
  11. package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
  12. package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
  13. package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
  14. package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
  15. package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
  16. package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
  17. package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
  18. package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
  19. package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
  20. package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
  21. package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
  22. package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
  23. package/dist/assets/client-CSk58DcF.js +7 -0
  24. package/dist/assets/config-D8KzikVB.js +1 -0
  25. package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
  26. package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
  27. package/dist/assets/dist-aTmhMDVh.js +9 -0
  28. package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
  29. package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
  30. package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
  31. package/dist/assets/i18n-C3jb83S6.js +1 -0
  32. package/dist/assets/index-CE4N7ItL.css +1 -0
  33. package/dist/assets/index-riX7Sg0_.js +6 -0
  34. package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
  35. package/dist/assets/loader-circle-BjMg63eu.js +1 -0
  36. package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
  37. package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
  38. package/dist/assets/plus-CIXME2pD.js +1 -0
  39. package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
  40. package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
  41. package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
  42. package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
  43. package/dist/assets/search-B_Qr0f6C.js +1 -0
  44. package/dist/assets/security-config-BGWYwxNr.js +1 -0
  45. package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
  46. package/dist/assets/skeleton-CYQJazv6.js +1 -0
  47. package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
  48. package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
  49. package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
  50. package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
  51. package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
  52. package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
  53. package/dist/assets/useMutation-BdZm-9PL.js +1 -0
  54. package/dist/assets/x-B8Tho_xC.js +1 -0
  55. package/dist/index.html +20 -18
  56. package/package.json +6 -6
  57. package/src/App.tsx +2 -0
  58. package/src/account/components/account-panel.tsx +46 -4
  59. package/src/account/managers/account.manager.ts +19 -4
  60. package/src/api/raw-client.test.ts +37 -0
  61. package/src/api/raw-client.ts +51 -8
  62. package/src/api/remote.ts +9 -0
  63. package/src/api/remote.types.ts +5 -0
  64. package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
  65. package/src/components/chat/ChatSidebar.test.tsx +109 -4
  66. package/src/components/chat/ChatSidebar.tsx +62 -9
  67. package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
  68. package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
  69. package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
  70. package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
  71. package/src/components/chat/chat-child-session-panel.tsx +155 -59
  72. package/src/components/chat/chat-page-runtime.test.ts +16 -19
  73. package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
  74. package/src/components/chat/chat-session-preference-sync.ts +9 -7
  75. package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
  76. package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
  77. package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
  78. package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
  79. package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
  80. package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
  81. package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
  82. package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
  83. package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
  84. package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
  85. package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
  86. package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
  87. package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
  88. package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
  89. package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
  90. package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
  91. package/src/components/chat/stores/chat-session-list.store.ts +99 -5
  92. package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
  93. package/src/components/chat/useChatSessionTypeState.ts +3 -5
  94. package/src/components/config/ChannelsList.test.tsx +68 -0
  95. package/src/components/config/ChannelsList.tsx +22 -4
  96. package/src/components/config/ProviderForm.tsx +9 -15
  97. package/src/components/config/ProvidersList.tsx +17 -3
  98. package/src/components/config/desktop-update-config.tsx +230 -0
  99. package/src/components/config/providers-list.test.tsx +68 -0
  100. package/src/components/layout/Sidebar.tsx +19 -14
  101. package/src/components/layout/sidebar.layout.test.tsx +33 -1
  102. package/src/components/marketplace/MarketplacePage.tsx +30 -30
  103. package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
  104. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
  105. package/src/desktop/desktop-update.types.ts +36 -0
  106. package/src/desktop/managers/desktop-update.manager.ts +163 -0
  107. package/src/desktop/stores/desktop-update.store.ts +18 -0
  108. package/src/hooks/marketplace-list-pages.ts +27 -0
  109. package/src/hooks/use-infinite-scroll-loader.ts +88 -0
  110. package/src/hooks/useMarketplace.ts +14 -3
  111. package/src/hooks/useMcpMarketplace.ts +14 -3
  112. package/src/lib/desktop-update-labels.utils.ts +72 -0
  113. package/src/lib/i18n.chat.ts +13 -0
  114. package/src/lib/i18n.remote.ts +15 -0
  115. package/src/lib/i18n.ts +3 -9
  116. package/src/lib/ui-document-title.ts +1 -0
  117. package/src/transport/local.transport.ts +57 -18
  118. package/src/vite-env.d.ts +10 -0
  119. package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
  120. package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
  121. package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
  122. package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
  123. package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
  124. package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
  125. package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
  126. package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
  127. package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
  128. package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
  129. package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
  130. package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
  131. package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
  132. package/dist/assets/config-CMiW0yaK.js +0 -1
  133. package/dist/assets/dist-BFc_H-lY.js +0 -15
  134. package/dist/assets/i18n-C_2dKw6w.js +0 -1
  135. package/dist/assets/index-ChUXhq0G.css +0 -1
  136. package/dist/assets/index-DAE8Srx-.js +0 -6
  137. package/dist/assets/label-D8yyejJS.js +0 -1
  138. package/dist/assets/loader-circle-B0sKKO29.js +0 -1
  139. package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
  140. package/dist/assets/plus-CYXs3JtZ.js +0 -1
  141. package/dist/assets/react-8EIEQjMP.js +0 -1
  142. package/dist/assets/search-DOsLw-P9.js +0 -1
  143. package/dist/assets/security-config-CM_tQRXQ.js +0 -1
  144. package/dist/assets/skeleton-GbHLjPC0.js +0 -1
  145. package/dist/assets/useMutation-DSinpgEq.js +0 -1
  146. package/dist/assets/x-Bnco_K8b.js +0 -1
  147. package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
  148. /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
  149. /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
@@ -1,13 +1,23 @@
1
- import { ArrowLeft, Loader2, X } from 'lucide-react';
2
- import { ChatMessageListContainer } from '@/components/chat/containers/chat-message-list.container';
3
- import { useNcpChildSessionTabsView } from '@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view';
4
- import { useNcpSessionConversation } from '@/components/chat/ncp/session-conversation/use-ncp-session-conversation';
5
- import type { ChatChildSessionTab } from '@/components/chat/stores/chat-thread.store';
6
- import { AgentIdentityAvatar } from '@/components/common/agent-identity';
7
- import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
8
- import { t } from '@/lib/i18n';
9
- import { cn } from '@/lib/utils';
10
- import type { ChatToolActionViewModel } from '@nextclaw/agent-chat-ui';
1
+ import { useEffect, useRef } from "react";
2
+ import { ArrowLeft, Loader2, X } from "lucide-react";
3
+ import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
4
+ import { ChatMessageListContainer } from "@/components/chat/containers/chat-message-list.container";
5
+ import {
6
+ useNcpChildSessionTabsView,
7
+ type ResolvedChildSessionTab,
8
+ } from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
9
+ import { useNcpSessionConversation } from "@/components/chat/ncp/session-conversation/use-ncp-session-conversation";
10
+ import {
11
+ shouldShowUnreadSessionIndicator,
12
+ useChatSessionListStore,
13
+ } from "@/components/chat/stores/chat-session-list.store";
14
+ import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
15
+ import type { ChatChildSessionTab } from "@/components/chat/stores/chat-thread.store";
16
+ import { AgentIdentityAvatar } from "@/components/common/agent-identity";
17
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
18
+ import { t } from "@/lib/i18n";
19
+ import { cn } from "@/lib/utils";
20
+ import type { ChatToolActionViewModel } from "@nextclaw/agent-chat-ui";
11
21
 
12
22
  type ChatChildSessionPanelProps = {
13
23
  tabs: readonly ChatChildSessionTab[];
@@ -27,39 +37,84 @@ function ChildSessionPanelConversation({
27
37
  }) {
28
38
  const agent = useNcpSessionConversation(sessionKey);
29
39
  const messages = agent.visibleMessages;
40
+ const scrollRef = useRef<HTMLDivElement>(null);
41
+ const { onScroll } = useStickyBottomScroll({
42
+ scrollRef,
43
+ resetKey: sessionKey,
44
+ isLoading: agent.isHydrating,
45
+ hasContent: messages.length > 0,
46
+ contentVersion: messages[messages.length - 1] ?? null,
47
+ stickyThresholdPx: 20,
48
+ });
30
49
 
31
- if (agent.isHydrating) {
32
- return (
33
- <div className="flex h-full items-center justify-center text-sm text-gray-500">
34
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
35
- {t('chatChildSessionLoading')}
36
- </div>
37
- );
38
- }
50
+ return (
51
+ <div
52
+ ref={scrollRef}
53
+ onScroll={onScroll}
54
+ className="h-full overflow-y-auto custom-scrollbar"
55
+ >
56
+ {agent.isHydrating ? (
57
+ <div className="flex h-full items-center justify-center text-sm text-gray-500">
58
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
59
+ {t("chatChildSessionLoading")}
60
+ </div>
61
+ ) : agent.hydrateError ? (
62
+ <div className="px-4 py-5 text-sm text-rose-600">
63
+ {agent.hydrateError.message}
64
+ </div>
65
+ ) : messages.length === 0 && !agent.isRunning ? (
66
+ <div className="px-4 py-5 text-sm text-gray-500">
67
+ {t("chatChildSessionEmpty")}
68
+ </div>
69
+ ) : (
70
+ <div className="px-4 py-5">
71
+ <ChatMessageListContainer
72
+ messages={messages}
73
+ isSending={agent.isRunning}
74
+ onToolAction={onToolAction}
75
+ />
76
+ </div>
77
+ )}
78
+ </div>
79
+ );
80
+ }
39
81
 
40
- if (agent.hydrateError) {
41
- return (
42
- <div className="px-4 py-5 text-sm text-rose-600">
43
- {agent.hydrateError.message}
44
- </div>
45
- );
46
- }
82
+ function ChildSessionPanelMetaChip({ value }: { value: string }) {
83
+ return (
84
+ <span className="inline-flex max-w-full items-center rounded-full border border-gray-200/80 bg-gray-50/90 px-2.5 py-1 text-[11px] font-medium text-gray-600">
85
+ <span className="truncate">{value}</span>
86
+ </span>
87
+ );
88
+ }
47
89
 
48
- if (messages.length === 0 && !agent.isRunning) {
49
- return (
50
- <div className="px-4 py-5 text-sm text-gray-500">
51
- {t('chatChildSessionEmpty')}
52
- </div>
53
- );
90
+ function ChildSessionPanelMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
91
+ const metaItems = [
92
+ tab.sessionTypeLabel,
93
+ tab.preferredModel,
94
+ tab.projectName,
95
+ ].filter((value): value is string => Boolean(value?.trim()));
96
+
97
+ if (metaItems.length === 0 && !tab.projectRoot) {
98
+ return null;
54
99
  }
55
100
 
56
101
  return (
57
- <div className="px-4 py-5">
58
- <ChatMessageListContainer
59
- messages={messages}
60
- isSending={agent.isRunning}
61
- onToolAction={onToolAction}
62
- />
102
+ <div className="mt-3 space-y-2">
103
+ {metaItems.length > 0 ? (
104
+ <div className="flex flex-wrap gap-1.5">
105
+ {metaItems.map((item) => (
106
+ <ChildSessionPanelMetaChip key={item} value={item} />
107
+ ))}
108
+ </div>
109
+ ) : null}
110
+ {tab.projectRoot ? (
111
+ <div
112
+ title={tab.projectRoot}
113
+ className="truncate rounded-xl border border-gray-200/70 bg-gray-50/80 px-2.5 py-2 font-mono text-[11px] text-gray-500"
114
+ >
115
+ {tab.projectRoot}
116
+ </div>
117
+ ) : null}
63
118
  </div>
64
119
  );
65
120
  }
@@ -72,14 +127,35 @@ export function ChatChildSessionPanel({
72
127
  onBackToParent,
73
128
  onToolAction,
74
129
  }: ChatChildSessionPanelProps) {
130
+ const presenter = usePresenter();
75
131
  const resolvedTabs = useNcpChildSessionTabsView(tabs);
132
+ const readUpdatedAtBySessionKey = useChatSessionListStore(
133
+ (state) => state.readUpdatedAtBySessionKey,
134
+ );
76
135
  const activeTab =
77
136
  resolvedTabs.find((tab) => tab.sessionKey === activeSessionKey) ??
78
137
  resolvedTabs[0] ??
79
138
  null;
80
- const hasParentSession = resolvedTabs.some((tab) => Boolean(tab.parentSessionKey));
139
+ const activeTabSessionKey = activeTab?.sessionKey ?? null;
140
+ const activeTabUpdatedAt = activeTab?.updatedAt?.trim() ?? null;
141
+ const hasParentSession = resolvedTabs.some((tab) =>
142
+ Boolean(tab.parentSessionKey),
143
+ );
81
144
  const shouldShowTabs = resolvedTabs.length > 1;
82
145
 
146
+ useEffect(() => {
147
+ const syncActiveTabReadState = () => {
148
+ if (!activeTabSessionKey || !activeTabUpdatedAt) {
149
+ return;
150
+ }
151
+ presenter.chatSessionListManager.markSessionRead(
152
+ activeTabSessionKey,
153
+ activeTabUpdatedAt,
154
+ );
155
+ };
156
+ syncActiveTabReadState();
157
+ }, [activeTabSessionKey, activeTabUpdatedAt, presenter]);
158
+
83
159
  if (!activeTab) {
84
160
  return null;
85
161
  }
@@ -93,18 +169,18 @@ export function ChatChildSessionPanel({
93
169
  type="button"
94
170
  onClick={onBackToParent}
95
171
  className={cn(
96
- 'inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900',
97
- !hasParentSession && 'pointer-events-none opacity-0',
172
+ "inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
173
+ !hasParentSession && "pointer-events-none opacity-0",
98
174
  )}
99
175
  >
100
176
  <ArrowLeft className="h-3.5 w-3.5" />
101
- <span>{t('chatBackToParent')}</span>
177
+ <span>{t("chatBackToParent")}</span>
102
178
  </button>
103
179
  <button
104
180
  type="button"
105
181
  onClick={onClose}
106
182
  className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
107
- aria-label={t('chatChildSessionClosePanel')}
183
+ aria-label={t("chatChildSessionClosePanel")}
108
184
  >
109
185
  <X className="h-4 w-4" />
110
186
  </button>
@@ -126,32 +202,52 @@ export function ChatChildSessionPanel({
126
202
  <div className="mt-3 overflow-x-auto custom-scrollbar">
127
203
  <Tabs value={activeSessionKey} onValueChange={onSelectSession}>
128
204
  <TabsList className="h-auto min-w-max justify-start gap-1.5 rounded-none bg-transparent p-0 text-gray-500">
129
- {resolvedTabs.map((tab) => (
130
- <TabsTrigger
131
- key={tab.sessionKey}
132
- value={tab.sessionKey}
133
- className="gap-2 rounded-full border border-gray-200/80 bg-white/85 px-2.5 py-1.5 text-xs font-medium text-gray-600 shadow-none hover:border-primary/30 hover:text-primary data-[state=active]:border-primary/30 data-[state=active]:bg-primary-50/70 data-[state=active]:text-primary data-[state=active]:shadow-sm"
134
- >
135
- {tab.agentId ? (
136
- <AgentIdentityAvatar
137
- agentId={tab.agentId}
138
- className="h-4 w-4 shrink-0"
139
- />
140
- ) : null}
141
- <span className="max-w-[132px] truncate">{tab.title}</span>
142
- </TabsTrigger>
143
- ))}
205
+ {resolvedTabs.map((tab) => {
206
+ const showUnreadDot = shouldShowUnreadSessionIndicator({
207
+ active: tab.sessionKey === activeSessionKey,
208
+ updatedAt: tab.updatedAt,
209
+ readUpdatedAt: readUpdatedAtBySessionKey[tab.sessionKey],
210
+ runStatus: tab.runStatus,
211
+ });
212
+ return (
213
+ <TabsTrigger
214
+ key={tab.sessionKey}
215
+ value={tab.sessionKey}
216
+ className="gap-2 rounded-full border border-gray-200/80 bg-white/85 px-2.5 py-1.5 text-xs font-medium text-gray-600 shadow-none hover:border-primary/30 hover:text-primary data-[state=active]:border-primary/30 data-[state=active]:bg-primary-50/70 data-[state=active]:text-primary data-[state=active]:shadow-sm"
217
+ >
218
+ {tab.agentId ? (
219
+ <AgentIdentityAvatar
220
+ agentId={tab.agentId}
221
+ className="h-4 w-4 shrink-0"
222
+ />
223
+ ) : null}
224
+ <span className="max-w-[132px] truncate">
225
+ {tab.title}
226
+ </span>
227
+ {showUnreadDot ? (
228
+ <span
229
+ aria-label={t("chatSessionUnread")}
230
+ className="h-2 w-2 shrink-0 rounded-full bg-primary"
231
+ />
232
+ ) : null}
233
+ </TabsTrigger>
234
+ );
235
+ })}
144
236
  </TabsList>
145
237
  </Tabs>
146
238
  </div>
147
239
  ) : null}
240
+ <ChildSessionPanelMetaStrip tab={activeTab} />
148
241
  </div>
149
242
 
150
- <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
243
+ <div className="flex-1 min-h-0">
151
244
  {resolvedTabs.map((tab) => (
152
245
  <div
153
246
  key={tab.sessionKey}
154
- className={cn(tab.sessionKey === activeSessionKey ? 'block' : 'hidden')}
247
+ className={cn(
248
+ "h-full",
249
+ tab.sessionKey === activeSessionKey ? "block" : "hidden",
250
+ )}
155
251
  >
156
252
  <ChildSessionPanelConversation
157
253
  sessionKey={tab.sessionKey}
@@ -6,7 +6,9 @@ import {
6
6
  resolveSelectedModelValue,
7
7
  resolveSelectedThinkingLevelValue
8
8
  } from '@/components/chat/chat-session-preference-governance';
9
- import { shouldRefreshDraftSessionId } from '@/components/chat/ncp/NcpChatPage';
9
+ import {
10
+ shouldClearPendingProjectRootOverride
11
+ } from '@/components/chat/ncp/NcpChatPage';
10
12
 
11
13
  const modelOptions = [
12
14
  {
@@ -155,33 +157,28 @@ describe('resolveSelectedModelValue', () => {
155
157
  });
156
158
  });
157
159
 
158
- describe('shouldRefreshDraftSessionId', () => {
159
- it('does not replace the initial draft session id on first mount', () => {
160
+ describe('shouldClearPendingProjectRootOverride', () => {
161
+ it('does not clear an unrelated session project override', () => {
160
162
  expect(
161
- shouldRefreshDraftSessionId({
162
- previousSelectedSessionKey: undefined,
163
- nextSelectedSessionKey: null
163
+ shouldClearPendingProjectRootOverride({
164
+ pendingProjectRoot: '/tmp/project-alpha',
165
+ pendingProjectRootSessionKey: 'draft-project-alpha',
166
+ sessionKey: 'session-existing',
167
+ selectedSessionProjectRoot: '/tmp/project-alpha'
164
168
  })
165
169
  ).toBe(false);
166
170
  });
167
171
 
168
- it('replaces the draft session id after leaving an existing session', () => {
172
+ it('clears the override only after the bound session reflects the same project root', () => {
169
173
  expect(
170
- shouldRefreshDraftSessionId({
171
- previousSelectedSessionKey: 'session-1',
172
- nextSelectedSessionKey: null
174
+ shouldClearPendingProjectRootOverride({
175
+ pendingProjectRoot: '/tmp/project-alpha',
176
+ pendingProjectRootSessionKey: 'draft-after-refresh',
177
+ sessionKey: 'draft-after-refresh',
178
+ selectedSessionProjectRoot: '/tmp/project-alpha'
173
179
  })
174
180
  ).toBe(true);
175
181
  });
176
-
177
- it('does not replace the draft session id while staying on the same session', () => {
178
- expect(
179
- shouldRefreshDraftSessionId({
180
- previousSelectedSessionKey: 'session-1',
181
- nextSelectedSessionKey: 'session-1'
182
- })
183
- ).toBe(false);
184
- });
185
182
  });
186
183
 
187
184
  describe('resolveRecentSessionPreferredModel', () => {
@@ -3,6 +3,7 @@ import { updateNcpSession } from '@/api/ncp-session';
3
3
  import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
4
4
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
5
5
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
6
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
6
7
 
7
8
  vi.mock('@/api/ncp-session', () => ({
8
9
  updateNcpSession: vi.fn(async () => ({
@@ -29,6 +30,12 @@ describe('ChatSessionPreferenceSync', () => {
29
30
  selectedSessionKey: null
30
31
  }
31
32
  }));
33
+ useChatThreadStore.setState((state) => ({
34
+ snapshot: {
35
+ ...state.snapshot,
36
+ canDeleteSession: false
37
+ }
38
+ }));
32
39
  vi.clearAllMocks();
33
40
  });
34
41
 
@@ -46,6 +53,12 @@ describe('ChatSessionPreferenceSync', () => {
46
53
  selectedSessionKey: 'session-1'
47
54
  }
48
55
  }));
56
+ useChatThreadStore.setState((state) => ({
57
+ snapshot: {
58
+ ...state.snapshot,
59
+ canDeleteSession: true
60
+ }
61
+ }));
49
62
 
50
63
  const sync = new ChatSessionPreferenceSync(updateNcpSession);
51
64
  sync.syncSelectedSessionPreferences();
@@ -1,6 +1,7 @@
1
1
  import type { SessionPatchUpdate, ThinkingLevel } from '@/api/types';
2
2
  import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
3
3
  import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
4
+ import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
4
5
 
5
6
  type QueuedSessionPreferenceSync = {
6
7
  sessionKey: string;
@@ -30,8 +31,9 @@ export class ChatSessionPreferenceSync {
30
31
  syncSelectedSessionPreferences = (): void => {
31
32
  const inputSnapshot = useChatInputStore.getState().snapshot;
32
33
  const sessionSnapshot = useChatSessionListStore.getState().snapshot;
34
+ const threadSnapshot = useChatThreadStore.getState().snapshot;
33
35
  const sessionKey = sessionSnapshot.selectedSessionKey;
34
- if (!sessionKey) {
36
+ if (!sessionKey || !threadSnapshot.canDeleteSession) {
35
37
  return;
36
38
  }
37
39
 
@@ -44,15 +46,15 @@ export class ChatSessionPreferenceSync {
44
46
  });
45
47
  };
46
48
 
47
- private enqueue(next: QueuedSessionPreferenceSync): void {
49
+ private enqueue = (next: QueuedSessionPreferenceSync): void => {
48
50
  this.queued = next;
49
51
  if (this.inFlight) {
50
52
  return;
51
53
  }
52
54
  this.startFlush();
53
- }
55
+ };
54
56
 
55
- private startFlush(): void {
57
+ private startFlush = (): void => {
56
58
  this.inFlight = this.flush()
57
59
  .catch((error) => {
58
60
  console.error(`Failed to sync chat session preferences: ${String(error)}`);
@@ -63,13 +65,13 @@ export class ChatSessionPreferenceSync {
63
65
  this.startFlush();
64
66
  }
65
67
  });
66
- }
68
+ };
67
69
 
68
- private async flush(): Promise<void> {
70
+ private flush = async (): Promise<void> => {
69
71
  while (this.queued) {
70
72
  const current = this.queued;
71
73
  this.queued = null;
72
74
  await this.updateSession(current.sessionKey, current.patch);
73
75
  }
74
- }
76
+ };
75
77
  }