@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.
- package/CHANGELOG.md +66 -0
- package/dist/assets/ChannelsList-D8p4OlM6.js +8 -0
- package/dist/assets/ChatPage-A45t1Rmf.js +58 -0
- package/dist/assets/DocBrowser-B2MpsnU9.js +1 -0
- package/dist/assets/{DocBrowser-NSzgVKka.js → DocBrowser-Cse_F8Nn.js} +1 -1
- package/dist/assets/{DocBrowserContext-DpgVdRgk.js → DocBrowserContext-Bai1WU2H.js} +1 -1
- package/dist/assets/{LogoBadge-CHS4YNLw.js → LogoBadge-BdxMPc9v.js} +1 -1
- package/dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
- package/dist/assets/MarketplacePage-BbpAkllU.js +49 -0
- package/dist/assets/McpMarketplacePage-CxPFOgxv.js +40 -0
- package/dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
- package/dist/assets/ProviderScopedModelInput-BYNouw-i.js +1 -0
- package/dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
- package/dist/assets/{RemoteAccessPage-yfbrveNQ.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
- package/dist/assets/RuntimeConfig-ChdfK4Y_.js +1 -0
- package/dist/assets/SearchConfig-DTeJvp8m.js +1 -0
- package/dist/assets/{SecretsConfig-CLFSSoTl.js → SecretsConfig-CCYO6NcV.js} +2 -2
- package/dist/assets/SessionsConfig-Du39vDgt.js +2 -0
- package/dist/assets/app-query-client-Dr5d-K8d.js +1 -0
- package/dist/assets/{book-open-C7TAghTk.js → book-open-Da4OEPqB.js} +1 -1
- package/dist/assets/chat-session-display-CAlPrnlV.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-DbL4EmiT.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
- package/dist/assets/client-CSk58DcF.js +7 -0
- package/dist/assets/config-D8KzikVB.js +1 -0
- package/dist/assets/{createLucideIcon-BRLFtf-8.js → createLucideIcon-83gaZMtv.js} +1 -1
- package/dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
- package/dist/assets/dist-aTmhMDVh.js +9 -0
- package/dist/assets/{dist-DP-JKR4G.js → dist-toEYs-MZ.js} +1 -1
- package/dist/assets/{external-link-BkJkiWbH.js → external-link-QQ0TC6X4.js} +1 -1
- package/dist/assets/{hash-CbP6-6R9.js → hash-DaFBEkmi.js} +1 -1
- package/dist/assets/i18n-C3jb83S6.js +1 -0
- package/dist/assets/index-CE4N7ItL.css +1 -0
- package/dist/assets/index-riX7Sg0_.js +6 -0
- package/dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
- package/dist/assets/loader-circle-BjMg63eu.js +1 -0
- package/dist/assets/{logos-N3dbS6-I.js → logos-Dzlz30M3.js} +1 -1
- package/dist/assets/{page-layout-DyuvlNrg.js → page-layout-D2eRufRQ.js} +1 -1
- package/dist/assets/plus-CIXME2pD.js +1 -0
- package/dist/assets/{popover-BKKWGUaG.js → popover-BSXxm5bj.js} +1 -1
- package/dist/assets/{refresh-ccw-BGMdiNGq.js → refresh-ccw-B3zMtN-_.js} +1 -1
- package/dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
- package/dist/assets/{save-Dh4GQzzX.js → save-Us9fg4Sj.js} +1 -1
- package/dist/assets/search-B_Qr0f6C.js +1 -0
- package/dist/assets/security-config-BGWYwxNr.js +1 -0
- package/dist/assets/{select-BtIi5fnh.js → select-DLYqySQK.js} +1 -1
- package/dist/assets/skeleton-CYQJazv6.js +1 -0
- package/dist/assets/{status-dot-C4O-2jZP.js → status-dot-DGayudyB.js} +1 -1
- package/dist/assets/{switch-DPegGIa_.js → switch-Dz2ScsKx.js} +1 -1
- package/dist/assets/{tabs-custom-x5GZexrF.js → tabs-custom-CdKyjiGk.js} +1 -1
- package/dist/assets/{trash-2-CU3LYIpQ.js → trash-2-Db-mZOZs.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
- package/dist/assets/{useConfirmDialog-S5WsGOGf.js → useConfirmDialog-DL0a-oGC.js} +1 -1
- package/dist/assets/useMutation-BdZm-9PL.js +1 -0
- package/dist/assets/x-B8Tho_xC.js +1 -0
- package/dist/index.html +20 -18
- package/package.json +6 -6
- package/src/App.tsx +2 -0
- package/src/account/components/account-panel.tsx +46 -4
- package/src/account/managers/account.manager.ts +19 -4
- package/src/api/raw-client.test.ts +37 -0
- package/src/api/raw-client.ts +51 -8
- package/src/api/remote.ts +9 -0
- package/src/api/remote.types.ts +5 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +344 -142
- package/src/components/chat/ChatSidebar.test.tsx +109 -4
- package/src/components/chat/ChatSidebar.tsx +62 -9
- package/src/components/chat/adapters/chat-message-tool-agent-id.test.ts +11 -11
- package/src/components/chat/adapters/chat-message.adapter.test.ts +43 -6
- package/src/components/chat/adapters/chat-message.session-request-tool-card.ts +182 -44
- package/src/components/chat/adapters/chat-message.session-spawn-tool-card.test.ts +104 -0
- package/src/components/chat/chat-child-session-panel.tsx +155 -59
- package/src/components/chat/chat-page-runtime.test.ts +16 -19
- package/src/components/chat/chat-session-preference-sync.test.ts +13 -0
- package/src/components/chat/chat-session-preference-sync.ts +9 -7
- package/src/components/chat/chat-sidebar-session-item.tsx +189 -121
- package/src/components/chat/containers/chat-message-list.container.test.tsx +21 -3
- package/src/components/chat/containers/chat-message-list.container.tsx +14 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +5 -5
- package/src/components/chat/hooks/use-chat-session-project.ts +0 -5
- package/src/components/chat/hooks/use-chat-session-update.test.tsx +75 -0
- package/src/components/chat/hooks/use-chat-session-update.ts +4 -2
- package/src/components/chat/managers/chat-session-list.manager.test.ts +79 -5
- package/src/components/chat/managers/chat-session-list.manager.ts +31 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +32 -51
- package/src/components/chat/ncp/ncp-app-client-fetch.test.ts +1 -1
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +45 -5
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +3 -5
- package/src/components/chat/ncp/ncp-chat-page-data.ts +0 -1
- package/src/components/chat/ncp/ncp-chat.presenter.ts +1 -11
- package/src/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view.ts +35 -9
- package/src/components/chat/stores/chat-session-list.store.ts +99 -5
- package/src/components/chat/useChatSessionTypeState.test.tsx +0 -3
- package/src/components/chat/useChatSessionTypeState.ts +3 -5
- package/src/components/config/ChannelsList.test.tsx +68 -0
- package/src/components/config/ChannelsList.tsx +22 -4
- package/src/components/config/ProviderForm.tsx +9 -15
- package/src/components/config/ProvidersList.tsx +17 -3
- package/src/components/config/desktop-update-config.tsx +230 -0
- package/src/components/config/providers-list.test.tsx +68 -0
- package/src/components/layout/Sidebar.tsx +19 -14
- package/src/components/layout/sidebar.layout.test.tsx +33 -1
- package/src/components/marketplace/MarketplacePage.tsx +30 -30
- package/src/components/marketplace/marketplace-page-parts.tsx +16 -24
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +28 -26
- package/src/desktop/desktop-update.types.ts +36 -0
- package/src/desktop/managers/desktop-update.manager.ts +163 -0
- package/src/desktop/stores/desktop-update.store.ts +18 -0
- package/src/hooks/marketplace-list-pages.ts +27 -0
- package/src/hooks/use-infinite-scroll-loader.ts +88 -0
- package/src/hooks/useMarketplace.ts +14 -3
- package/src/hooks/useMcpMarketplace.ts +14 -3
- package/src/lib/desktop-update-labels.utils.ts +72 -0
- package/src/lib/i18n.chat.ts +13 -0
- package/src/lib/i18n.remote.ts +15 -0
- package/src/lib/i18n.ts +3 -9
- package/src/lib/ui-document-title.ts +1 -0
- package/src/transport/local.transport.ts +57 -18
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/ChannelsList-CobWeI2V.js +0 -8
- package/dist/assets/ChatPage-ZIdFFVAv.js +0 -43
- package/dist/assets/DocBrowser-D55C0iyl.js +0 -1
- package/dist/assets/MarketplacePage-BFYsRss_.js +0 -49
- package/dist/assets/MarketplacePage-DII-q-Y1.js +0 -1
- package/dist/assets/McpMarketplacePage-CPqsGJzz.js +0 -40
- package/dist/assets/ModelConfig-Bvuo_IpS.js +0 -1
- package/dist/assets/ProviderScopedModelInput-BfY8rGsf.js +0 -1
- package/dist/assets/ProvidersList-3tlaqwSS.js +0 -1
- package/dist/assets/RuntimeConfig-CAd5Kta3.js +0 -1
- package/dist/assets/SearchConfig-DFwgaAa7.js +0 -1
- package/dist/assets/SessionsConfig-vYrvc2Fk.js +0 -2
- package/dist/assets/chat-session-display-5dVFkJyw.js +0 -1
- package/dist/assets/config-CMiW0yaK.js +0 -1
- package/dist/assets/dist-BFc_H-lY.js +0 -15
- package/dist/assets/i18n-C_2dKw6w.js +0 -1
- package/dist/assets/index-ChUXhq0G.css +0 -1
- package/dist/assets/index-DAE8Srx-.js +0 -6
- package/dist/assets/label-D8yyejJS.js +0 -1
- package/dist/assets/loader-circle-B0sKKO29.js +0 -1
- package/dist/assets/marketplace-localization-CxSTG9wr.js +0 -1
- package/dist/assets/plus-CYXs3JtZ.js +0 -1
- package/dist/assets/react-8EIEQjMP.js +0 -1
- package/dist/assets/search-DOsLw-P9.js +0 -1
- package/dist/assets/security-config-CM_tQRXQ.js +0 -1
- package/dist/assets/skeleton-GbHLjPC0.js +0 -1
- package/dist/assets/useMutation-DSinpgEq.js +0 -1
- package/dist/assets/x-Bnco_K8b.js +0 -1
- package/src/components/chat/ChatSessionsSidebar.tsx +0 -100
- /package/dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
- /package/dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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="
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
97
|
-
!hasParentSession &&
|
|
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(
|
|
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(
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
243
|
+
<div className="flex-1 min-h-0">
|
|
151
244
|
{resolvedTabs.map((tab) => (
|
|
152
245
|
<div
|
|
153
246
|
key={tab.sessionKey}
|
|
154
|
-
className={cn(
|
|
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 {
|
|
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('
|
|
159
|
-
it('does not
|
|
160
|
+
describe('shouldClearPendingProjectRootOverride', () => {
|
|
161
|
+
it('does not clear an unrelated session project override', () => {
|
|
160
162
|
expect(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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('
|
|
172
|
+
it('clears the override only after the bound session reflects the same project root', () => {
|
|
169
173
|
expect(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|
}
|