@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,10 +1,12 @@
|
|
|
1
|
-
import { render, screen } from
|
|
2
|
-
import userEvent from
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from
|
|
4
|
-
import { ChatChildSessionPanel } from
|
|
5
|
-
import { ChatConversationPanel } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatChildSessionPanel } from "@/components/chat/chat-child-session-panel";
|
|
5
|
+
import { ChatConversationPanel } from "@/components/chat/ChatConversationPanel";
|
|
6
|
+
import type { ResolvedChildSessionTab } from "@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view";
|
|
7
|
+
import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
|
|
8
|
+
import { useChatSessionListStore } from "@/components/chat/stores/chat-session-list.store";
|
|
9
|
+
import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
|
|
8
10
|
|
|
9
11
|
const mocks = vi.hoisted(() => ({
|
|
10
12
|
deleteSession: vi.fn(),
|
|
@@ -12,41 +14,45 @@ const mocks = vi.hoisted(() => ({
|
|
|
12
14
|
createSession: vi.fn(),
|
|
13
15
|
setSelectedAgentId: vi.fn(),
|
|
14
16
|
setPendingSessionType: vi.fn(),
|
|
17
|
+
stickyBottomScroll: vi.fn(() => ({
|
|
18
|
+
onScroll: vi.fn(),
|
|
19
|
+
})),
|
|
15
20
|
resolvedChildTabs: [
|
|
16
21
|
{
|
|
17
|
-
sessionKey:
|
|
18
|
-
parentSessionKey:
|
|
19
|
-
title:
|
|
20
|
-
agentId:
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
sessionKey: "child-session-1",
|
|
23
|
+
parentSessionKey: "parent-session-1",
|
|
24
|
+
title: "北京天气",
|
|
25
|
+
agentId: "weather",
|
|
26
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
27
|
+
sessionTypeLabel: "Codex",
|
|
28
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
29
|
+
projectName: "project-alpha",
|
|
30
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
23
31
|
},
|
|
24
|
-
],
|
|
32
|
+
] as ResolvedChildSessionTab[],
|
|
25
33
|
}));
|
|
26
34
|
|
|
27
|
-
vi.mock(
|
|
35
|
+
vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
28
36
|
const actual = await importOriginal();
|
|
29
37
|
return {
|
|
30
38
|
...(actual as object),
|
|
31
|
-
useStickyBottomScroll:
|
|
32
|
-
onScroll: vi.fn()
|
|
33
|
-
})
|
|
39
|
+
useStickyBottomScroll: mocks.stickyBottomScroll,
|
|
34
40
|
};
|
|
35
41
|
});
|
|
36
42
|
|
|
37
|
-
vi.mock(
|
|
43
|
+
vi.mock("@/components/chat/nextclaw", () => ({
|
|
38
44
|
ChatInputBarContainer: () => <div data-testid="chat-input-bar" />,
|
|
39
|
-
ChatMessageListContainer: () => <div data-testid="chat-message-list"
|
|
45
|
+
ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
|
|
40
46
|
}));
|
|
41
47
|
|
|
42
|
-
vi.mock(
|
|
48
|
+
vi.mock("@/components/chat/containers/chat-message-list.container", () => ({
|
|
43
49
|
ChatMessageListContainer: () => <div data-testid="child-chat-message-list" />,
|
|
44
50
|
}));
|
|
45
51
|
|
|
46
|
-
vi.mock(
|
|
52
|
+
vi.mock("@/components/chat/ChatWelcome", () => ({
|
|
47
53
|
ChatWelcome: ({
|
|
48
54
|
onCreateSession,
|
|
49
|
-
onSelectAgent
|
|
55
|
+
onSelectAgent,
|
|
50
56
|
}: {
|
|
51
57
|
onCreateSession: () => void;
|
|
52
58
|
onSelectAgent: (agentId: string) => void;
|
|
@@ -55,87 +61,115 @@ vi.mock('@/components/chat/ChatWelcome', () => ({
|
|
|
55
61
|
<button type="button" onClick={onCreateSession}>
|
|
56
62
|
create draft session
|
|
57
63
|
</button>
|
|
58
|
-
<button type="button" onClick={() => onSelectAgent(
|
|
64
|
+
<button type="button" onClick={() => onSelectAgent("engineer")}>
|
|
59
65
|
switch draft agent
|
|
60
66
|
</button>
|
|
61
67
|
</div>
|
|
62
|
-
)
|
|
68
|
+
),
|
|
63
69
|
}));
|
|
64
70
|
|
|
65
|
-
vi.mock(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
vi.mock("@/components/chat/presenter/chat-presenter-context", () => ({
|
|
72
|
+
usePresenter: () => ({
|
|
73
|
+
chatThreadManager: {
|
|
74
|
+
deleteSession: mocks.deleteSession,
|
|
75
|
+
goToProviders: mocks.goToProviders,
|
|
76
|
+
openSessionFromToolAction: vi.fn(),
|
|
77
|
+
selectChildSessionDetail: vi.fn(),
|
|
78
|
+
closeChildSessionDetail: vi.fn(),
|
|
79
|
+
goToParentSession: vi.fn(),
|
|
80
|
+
},
|
|
75
81
|
chatSessionListManager: {
|
|
76
82
|
selectSession: vi.fn(),
|
|
77
83
|
createSession: mocks.createSession,
|
|
78
|
-
setSelectedAgentId: mocks.setSelectedAgentId
|
|
84
|
+
setSelectedAgentId: mocks.setSelectedAgentId,
|
|
85
|
+
markSessionRead: (
|
|
86
|
+
sessionKey: string | null | undefined,
|
|
87
|
+
updatedAt: string | null | undefined,
|
|
88
|
+
) =>
|
|
89
|
+
sessionKey
|
|
90
|
+
? useChatSessionListStore.getState().markSessionRead(
|
|
91
|
+
sessionKey,
|
|
92
|
+
updatedAt,
|
|
93
|
+
)
|
|
94
|
+
: undefined,
|
|
95
|
+
hydrateReadWatermarks: (
|
|
96
|
+
entries: readonly { sessionKey: string; updatedAt: string | null | undefined }[],
|
|
97
|
+
) => useChatSessionListStore.getState().hydrateReadWatermarks(entries),
|
|
79
98
|
},
|
|
80
99
|
chatInputManager: {
|
|
81
|
-
setPendingSessionType: mocks.setPendingSessionType
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
vi.mock('@/components/chat/session-header/chat-session-header-actions', () => ({
|
|
87
|
-
ChatSessionHeaderActions: () => <button aria-label="More actions" />
|
|
100
|
+
setPendingSessionType: mocks.setPendingSessionType,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
88
103
|
}));
|
|
89
104
|
|
|
90
|
-
vi.mock(
|
|
91
|
-
|
|
105
|
+
vi.mock("@/components/chat/session-header/chat-session-header-actions", () => ({
|
|
106
|
+
ChatSessionHeaderActions: () => <button aria-label="More actions" />,
|
|
92
107
|
}));
|
|
93
108
|
|
|
94
|
-
vi.mock(
|
|
95
|
-
|
|
109
|
+
vi.mock("@/components/chat/session-header/chat-session-project-badge", () => ({
|
|
110
|
+
ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => (
|
|
111
|
+
<button>{projectName}</button>
|
|
112
|
+
),
|
|
96
113
|
}));
|
|
97
114
|
|
|
98
|
-
vi.mock(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
hydrateError: null,
|
|
103
|
-
isRunning: false,
|
|
115
|
+
vi.mock(
|
|
116
|
+
"@/components/chat/ncp/session-conversation/use-ncp-child-session-tabs-view",
|
|
117
|
+
() => ({
|
|
118
|
+
useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
|
|
104
119
|
}),
|
|
105
|
-
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
vi.mock(
|
|
123
|
+
"@/components/chat/ncp/session-conversation/use-ncp-session-conversation",
|
|
124
|
+
() => ({
|
|
125
|
+
useNcpSessionConversation: () => ({
|
|
126
|
+
visibleMessages: [],
|
|
127
|
+
isHydrating: false,
|
|
128
|
+
hydrateError: null,
|
|
129
|
+
isRunning: false,
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
106
133
|
|
|
107
|
-
vi.mock(
|
|
134
|
+
vi.mock("@/components/common/AgentAvatar", () => ({
|
|
108
135
|
AgentAvatar: ({ agentId }: { agentId: string }) => (
|
|
109
136
|
<div data-testid="agent-avatar">{agentId}</div>
|
|
110
137
|
),
|
|
111
138
|
}));
|
|
112
139
|
|
|
113
|
-
vi.mock(
|
|
140
|
+
vi.mock("@/components/common/agent-identity", () => ({
|
|
114
141
|
AgentIdentityAvatar: ({ agentId }: { agentId: string }) => (
|
|
115
142
|
<div data-testid="agent-identity-avatar">{agentId}</div>
|
|
116
143
|
),
|
|
117
144
|
}));
|
|
118
145
|
|
|
119
|
-
describe(
|
|
146
|
+
describe("ChatConversationPanel", () => {
|
|
120
147
|
beforeEach(() => {
|
|
121
148
|
mocks.deleteSession.mockReset();
|
|
122
149
|
mocks.goToProviders.mockReset();
|
|
123
150
|
mocks.createSession.mockReset();
|
|
124
151
|
mocks.setSelectedAgentId.mockReset();
|
|
125
152
|
mocks.setPendingSessionType.mockReset();
|
|
153
|
+
mocks.stickyBottomScroll.mockClear();
|
|
126
154
|
useChatInputStore.setState({
|
|
127
155
|
snapshot: {
|
|
128
156
|
...useChatInputStore.getState().snapshot,
|
|
129
|
-
defaultSessionType:
|
|
130
|
-
}
|
|
157
|
+
defaultSessionType: "native",
|
|
158
|
+
},
|
|
131
159
|
});
|
|
132
160
|
useChatThreadStore.setState({
|
|
133
161
|
snapshot: {
|
|
134
162
|
...useChatThreadStore.getState().snapshot,
|
|
135
163
|
isProviderStateResolved: true,
|
|
136
|
-
modelOptions: [
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
modelOptions: [
|
|
165
|
+
{
|
|
166
|
+
value: "openai/gpt-5.1",
|
|
167
|
+
modelLabel: "gpt-5.1",
|
|
168
|
+
providerLabel: "OpenAI",
|
|
169
|
+
} as never,
|
|
170
|
+
],
|
|
171
|
+
sessionTypeLabel: "Codex",
|
|
172
|
+
sessionKey: "draft-session-1",
|
|
139
173
|
sessionDisplayName: undefined,
|
|
140
174
|
agentId: null,
|
|
141
175
|
agentDisplayName: null,
|
|
@@ -150,121 +184,135 @@ describe('ChatConversationPanel', () => {
|
|
|
150
184
|
parentSessionKey: null,
|
|
151
185
|
parentSessionLabel: null,
|
|
152
186
|
availableAgents: [
|
|
153
|
-
{ id:
|
|
154
|
-
{ id:
|
|
187
|
+
{ id: "main", displayName: "Main", runtime: "native" },
|
|
188
|
+
{ id: "engineer", displayName: "Engineer", runtime: "codex" },
|
|
155
189
|
],
|
|
156
190
|
childSessionTabs: [],
|
|
157
191
|
activeChildSessionKey: null,
|
|
158
|
-
}
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
useChatSessionListStore.setState({
|
|
195
|
+
readUpdatedAtBySessionKey: {},
|
|
196
|
+
hasHydratedReadWatermarks: false,
|
|
197
|
+
snapshot: {
|
|
198
|
+
...useChatSessionListStore.getState().snapshot,
|
|
199
|
+
},
|
|
159
200
|
});
|
|
160
201
|
});
|
|
161
202
|
|
|
162
|
-
it(
|
|
203
|
+
it("shows the draft session type in the conversation header", () => {
|
|
163
204
|
render(<ChatConversationPanel />);
|
|
164
205
|
|
|
165
|
-
expect(screen.getByText(
|
|
166
|
-
expect(screen.getByText(
|
|
167
|
-
expect(screen.getByLabelText(
|
|
206
|
+
expect(screen.getByText("New Task")).toBeTruthy();
|
|
207
|
+
expect(screen.getByText("Codex")).toBeTruthy();
|
|
208
|
+
expect(screen.getByLabelText("More actions")).toBeTruthy();
|
|
168
209
|
});
|
|
169
210
|
|
|
170
|
-
it(
|
|
211
|
+
it("shows the selected session project badge and more actions trigger", () => {
|
|
171
212
|
useChatThreadStore.setState({
|
|
172
213
|
snapshot: {
|
|
173
214
|
...useChatThreadStore.getState().snapshot,
|
|
174
|
-
sessionKey:
|
|
175
|
-
sessionDisplayName:
|
|
176
|
-
sessionProjectRoot:
|
|
177
|
-
sessionProjectName:
|
|
215
|
+
sessionKey: "session-1",
|
|
216
|
+
sessionDisplayName: "Project Thread",
|
|
217
|
+
sessionProjectRoot: "/Users/demo/workspace/project-alpha",
|
|
218
|
+
sessionProjectName: "project-alpha",
|
|
178
219
|
canDeleteSession: true,
|
|
179
|
-
}
|
|
220
|
+
},
|
|
180
221
|
});
|
|
181
222
|
|
|
182
223
|
render(<ChatConversationPanel />);
|
|
183
224
|
|
|
184
|
-
expect(screen.getByText(
|
|
185
|
-
expect(screen.getByText(
|
|
186
|
-
expect(screen.getByLabelText(
|
|
225
|
+
expect(screen.getByText("Project Thread")).toBeTruthy();
|
|
226
|
+
expect(screen.getByText("project-alpha")).toBeTruthy();
|
|
227
|
+
expect(screen.getByLabelText("More actions")).toBeTruthy();
|
|
187
228
|
});
|
|
188
229
|
|
|
189
|
-
it(
|
|
230
|
+
it("does not show a header agent marker for the main agent", () => {
|
|
190
231
|
useChatThreadStore.setState({
|
|
191
232
|
snapshot: {
|
|
192
233
|
...useChatThreadStore.getState().snapshot,
|
|
193
|
-
agentId:
|
|
194
|
-
agentDisplayName:
|
|
195
|
-
}
|
|
234
|
+
agentId: "main",
|
|
235
|
+
agentDisplayName: "Main",
|
|
236
|
+
},
|
|
196
237
|
});
|
|
197
238
|
|
|
198
239
|
render(<ChatConversationPanel />);
|
|
199
240
|
|
|
200
|
-
expect(screen.queryByTestId(
|
|
241
|
+
expect(screen.queryByTestId("agent-avatar")).toBeNull();
|
|
201
242
|
});
|
|
202
243
|
|
|
203
|
-
it(
|
|
244
|
+
it("shows only a lightweight avatar marker for a specialist agent", () => {
|
|
204
245
|
useChatThreadStore.setState({
|
|
205
246
|
snapshot: {
|
|
206
247
|
...useChatThreadStore.getState().snapshot,
|
|
207
|
-
agentId:
|
|
208
|
-
agentDisplayName:
|
|
209
|
-
}
|
|
248
|
+
agentId: "engineer",
|
|
249
|
+
agentDisplayName: "Engineer",
|
|
250
|
+
},
|
|
210
251
|
});
|
|
211
252
|
|
|
212
253
|
render(<ChatConversationPanel />);
|
|
213
254
|
|
|
214
|
-
expect(screen.getByTestId(
|
|
215
|
-
expect(screen.queryByText(
|
|
255
|
+
expect(screen.getByTestId("agent-avatar").textContent).toBe("engineer");
|
|
256
|
+
expect(screen.queryByText("Engineer")).toBeNull();
|
|
216
257
|
});
|
|
217
258
|
|
|
218
|
-
it(
|
|
259
|
+
it("creates a draft session with the selected draft agent runtime", async () => {
|
|
219
260
|
const user = userEvent.setup();
|
|
220
261
|
|
|
221
262
|
useChatThreadStore.setState({
|
|
222
263
|
snapshot: {
|
|
223
264
|
...useChatThreadStore.getState().snapshot,
|
|
224
|
-
agentId:
|
|
225
|
-
agentDisplayName:
|
|
226
|
-
}
|
|
265
|
+
agentId: "engineer",
|
|
266
|
+
agentDisplayName: "Engineer",
|
|
267
|
+
},
|
|
227
268
|
});
|
|
228
269
|
|
|
229
270
|
render(<ChatConversationPanel />);
|
|
230
271
|
|
|
231
|
-
await user.click(
|
|
272
|
+
await user.click(
|
|
273
|
+
screen.getByRole("button", { name: "create draft session" }),
|
|
274
|
+
);
|
|
232
275
|
|
|
233
|
-
expect(mocks.createSession).toHaveBeenCalledWith(
|
|
276
|
+
expect(mocks.createSession).toHaveBeenCalledWith("codex");
|
|
234
277
|
});
|
|
235
278
|
|
|
236
|
-
it(
|
|
279
|
+
it("syncs the pending session type when switching the draft agent", async () => {
|
|
237
280
|
const user = userEvent.setup();
|
|
238
281
|
|
|
239
282
|
render(<ChatConversationPanel />);
|
|
240
283
|
|
|
241
|
-
await user.click(
|
|
284
|
+
await user.click(
|
|
285
|
+
screen.getByRole("button", { name: "switch draft agent" }),
|
|
286
|
+
);
|
|
242
287
|
|
|
243
|
-
expect(mocks.setSelectedAgentId).toHaveBeenCalledWith(
|
|
244
|
-
expect(mocks.setPendingSessionType).toHaveBeenCalledWith(
|
|
288
|
+
expect(mocks.setSelectedAgentId).toHaveBeenCalledWith("engineer");
|
|
289
|
+
expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
|
|
245
290
|
});
|
|
246
291
|
});
|
|
247
292
|
|
|
248
|
-
describe(
|
|
249
|
-
it(
|
|
293
|
+
describe("ChatChildSessionPanel", () => {
|
|
294
|
+
it("keeps the header compact for a single child session", () => {
|
|
250
295
|
mocks.resolvedChildTabs = [
|
|
251
296
|
{
|
|
252
|
-
sessionKey:
|
|
253
|
-
parentSessionKey:
|
|
254
|
-
title:
|
|
255
|
-
agentId:
|
|
256
|
-
|
|
257
|
-
|
|
297
|
+
sessionKey: "child-session-1",
|
|
298
|
+
parentSessionKey: "parent-session-1",
|
|
299
|
+
title: "北京天气",
|
|
300
|
+
agentId: "weather",
|
|
301
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
302
|
+
sessionTypeLabel: "Codex",
|
|
303
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
304
|
+
projectName: "project-alpha",
|
|
305
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
258
306
|
},
|
|
259
307
|
];
|
|
260
308
|
render(
|
|
261
309
|
<ChatChildSessionPanel
|
|
262
310
|
tabs={[
|
|
263
311
|
{
|
|
264
|
-
sessionKey:
|
|
265
|
-
parentSessionKey:
|
|
266
|
-
label:
|
|
267
|
-
agentId:
|
|
312
|
+
sessionKey: "child-session-1",
|
|
313
|
+
parentSessionKey: "parent-session-1",
|
|
314
|
+
label: "北京天气",
|
|
315
|
+
agentId: "weather",
|
|
268
316
|
},
|
|
269
317
|
]}
|
|
270
318
|
activeSessionKey="child-session-1"
|
|
@@ -274,28 +322,44 @@ describe('ChatChildSessionPanel', () => {
|
|
|
274
322
|
/>,
|
|
275
323
|
);
|
|
276
324
|
|
|
277
|
-
expect(screen.getByText(
|
|
278
|
-
expect(screen.
|
|
279
|
-
expect(screen.
|
|
325
|
+
expect(screen.getByText("北京天气")).toBeTruthy();
|
|
326
|
+
expect(screen.getByText("Codex")).toBeTruthy();
|
|
327
|
+
expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
|
|
328
|
+
expect(screen.getByText("project-alpha")).toBeTruthy();
|
|
329
|
+
expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
|
|
330
|
+
expect(screen.queryByText("Child Sessions")).toBeNull();
|
|
331
|
+
expect(screen.queryByText("child-session-1")).toBeNull();
|
|
332
|
+
expect(mocks.stickyBottomScroll).toHaveBeenCalledWith(
|
|
333
|
+
expect.objectContaining({
|
|
334
|
+
resetKey: "child-session-1",
|
|
335
|
+
stickyThresholdPx: 20,
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
280
338
|
});
|
|
281
339
|
|
|
282
|
-
it(
|
|
340
|
+
it("uses tabs as the only title layer when multiple child sessions are open", () => {
|
|
283
341
|
mocks.resolvedChildTabs = [
|
|
284
342
|
{
|
|
285
|
-
sessionKey:
|
|
286
|
-
parentSessionKey:
|
|
287
|
-
title:
|
|
288
|
-
agentId:
|
|
289
|
-
|
|
290
|
-
|
|
343
|
+
sessionKey: "child-session-1",
|
|
344
|
+
parentSessionKey: "parent-session-1",
|
|
345
|
+
title: "北京天气",
|
|
346
|
+
agentId: "weather",
|
|
347
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
348
|
+
sessionTypeLabel: "Codex",
|
|
349
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
350
|
+
projectName: "project-alpha",
|
|
351
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
291
352
|
},
|
|
292
353
|
{
|
|
293
|
-
sessionKey:
|
|
294
|
-
parentSessionKey:
|
|
295
|
-
title:
|
|
296
|
-
agentId:
|
|
297
|
-
|
|
298
|
-
|
|
354
|
+
sessionKey: "child-session-2",
|
|
355
|
+
parentSessionKey: "parent-session-1",
|
|
356
|
+
title: "上海天气",
|
|
357
|
+
agentId: "weather",
|
|
358
|
+
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
359
|
+
sessionTypeLabel: "Claude Code",
|
|
360
|
+
preferredModel: "anthropic/claude-sonnet-4",
|
|
361
|
+
projectName: "project-beta",
|
|
362
|
+
projectRoot: "/Users/demo/project-beta",
|
|
299
363
|
},
|
|
300
364
|
];
|
|
301
365
|
|
|
@@ -303,16 +367,16 @@ describe('ChatChildSessionPanel', () => {
|
|
|
303
367
|
<ChatChildSessionPanel
|
|
304
368
|
tabs={[
|
|
305
369
|
{
|
|
306
|
-
sessionKey:
|
|
307
|
-
parentSessionKey:
|
|
308
|
-
label:
|
|
309
|
-
agentId:
|
|
370
|
+
sessionKey: "child-session-1",
|
|
371
|
+
parentSessionKey: "parent-session-1",
|
|
372
|
+
label: "北京天气",
|
|
373
|
+
agentId: "weather",
|
|
310
374
|
},
|
|
311
375
|
{
|
|
312
|
-
sessionKey:
|
|
313
|
-
parentSessionKey:
|
|
314
|
-
label:
|
|
315
|
-
agentId:
|
|
376
|
+
sessionKey: "child-session-2",
|
|
377
|
+
parentSessionKey: "parent-session-1",
|
|
378
|
+
label: "上海天气",
|
|
379
|
+
agentId: "weather",
|
|
316
380
|
},
|
|
317
381
|
]}
|
|
318
382
|
activeSessionKey="child-session-1"
|
|
@@ -322,13 +386,151 @@ describe('ChatChildSessionPanel', () => {
|
|
|
322
386
|
/>,
|
|
323
387
|
);
|
|
324
388
|
|
|
325
|
-
expect(screen.getAllByText(
|
|
326
|
-
expect(screen.getByText(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
);
|
|
389
|
+
expect(screen.getAllByText("北京天气")).toHaveLength(1);
|
|
390
|
+
expect(screen.getByText("上海天气")).toBeTruthy();
|
|
391
|
+
expect(screen.getByText("Codex")).toBeTruthy();
|
|
392
|
+
expect(screen.getByText("openai/gpt-5.3-codex")).toBeTruthy();
|
|
393
|
+
expect(screen.getByText("project-alpha")).toBeTruthy();
|
|
394
|
+
expect(screen.getByText("/Users/demo/project-alpha")).toBeTruthy();
|
|
395
|
+
const tabButtons = screen
|
|
396
|
+
.getAllByRole("button")
|
|
397
|
+
.filter((element) => element.getAttribute("aria-pressed") !== null);
|
|
330
398
|
expect(tabButtons).toHaveLength(2);
|
|
331
|
-
expect(tabButtons[0]?.getAttribute(
|
|
332
|
-
expect(tabButtons[1]?.getAttribute(
|
|
399
|
+
expect(tabButtons[0]?.getAttribute("aria-pressed")).toBe("true");
|
|
400
|
+
expect(tabButtons[1]?.getAttribute("aria-pressed")).toBe("false");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("shows an unread dot for inactive child tabs until the user opens them", () => {
|
|
404
|
+
mocks.resolvedChildTabs = [
|
|
405
|
+
{
|
|
406
|
+
sessionKey: "child-session-1",
|
|
407
|
+
parentSessionKey: "parent-session-1",
|
|
408
|
+
title: "北京天气",
|
|
409
|
+
agentId: "weather",
|
|
410
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
411
|
+
sessionTypeLabel: "Codex",
|
|
412
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
413
|
+
projectName: "project-alpha",
|
|
414
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
sessionKey: "child-session-2",
|
|
418
|
+
parentSessionKey: "parent-session-1",
|
|
419
|
+
title: "上海天气",
|
|
420
|
+
agentId: "weather",
|
|
421
|
+
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
422
|
+
runStatus: "running",
|
|
423
|
+
sessionTypeLabel: "Claude Code",
|
|
424
|
+
preferredModel: "anthropic/claude-sonnet-4",
|
|
425
|
+
projectName: "project-beta",
|
|
426
|
+
projectRoot: "/Users/demo/project-beta",
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
const { rerender } = render(
|
|
431
|
+
<ChatChildSessionPanel
|
|
432
|
+
tabs={[
|
|
433
|
+
{
|
|
434
|
+
sessionKey: "child-session-1",
|
|
435
|
+
parentSessionKey: "parent-session-1",
|
|
436
|
+
label: "北京天气",
|
|
437
|
+
agentId: "weather",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
sessionKey: "child-session-2",
|
|
441
|
+
parentSessionKey: "parent-session-1",
|
|
442
|
+
label: "上海天气",
|
|
443
|
+
agentId: "weather",
|
|
444
|
+
},
|
|
445
|
+
]}
|
|
446
|
+
activeSessionKey="child-session-1"
|
|
447
|
+
onSelectSession={vi.fn()}
|
|
448
|
+
onClose={vi.fn()}
|
|
449
|
+
onBackToParent={vi.fn()}
|
|
450
|
+
/>,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(
|
|
454
|
+
screen.queryByLabelText("Session has unread updates"),
|
|
455
|
+
).toBeNull();
|
|
456
|
+
|
|
457
|
+
mocks.resolvedChildTabs = [
|
|
458
|
+
{
|
|
459
|
+
sessionKey: "child-session-1",
|
|
460
|
+
parentSessionKey: "parent-session-1",
|
|
461
|
+
title: "北京天气",
|
|
462
|
+
agentId: "weather",
|
|
463
|
+
updatedAt: "2026-04-10T09:00:00.000Z",
|
|
464
|
+
sessionTypeLabel: "Codex",
|
|
465
|
+
preferredModel: "openai/gpt-5.3-codex",
|
|
466
|
+
projectName: "project-alpha",
|
|
467
|
+
projectRoot: "/Users/demo/project-alpha",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
sessionKey: "child-session-2",
|
|
471
|
+
parentSessionKey: "parent-session-1",
|
|
472
|
+
title: "上海天气",
|
|
473
|
+
agentId: "weather",
|
|
474
|
+
updatedAt: "2026-04-10T09:05:00.000Z",
|
|
475
|
+
sessionTypeLabel: "Claude Code",
|
|
476
|
+
preferredModel: "anthropic/claude-sonnet-4",
|
|
477
|
+
projectName: "project-beta",
|
|
478
|
+
projectRoot: "/Users/demo/project-beta",
|
|
479
|
+
},
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
rerender(
|
|
483
|
+
<ChatChildSessionPanel
|
|
484
|
+
tabs={[
|
|
485
|
+
{
|
|
486
|
+
sessionKey: "child-session-1",
|
|
487
|
+
parentSessionKey: "parent-session-1",
|
|
488
|
+
label: "北京天气",
|
|
489
|
+
agentId: "weather",
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
sessionKey: "child-session-2",
|
|
493
|
+
parentSessionKey: "parent-session-1",
|
|
494
|
+
label: "上海天气",
|
|
495
|
+
agentId: "weather",
|
|
496
|
+
},
|
|
497
|
+
]}
|
|
498
|
+
activeSessionKey="child-session-1"
|
|
499
|
+
onSelectSession={vi.fn()}
|
|
500
|
+
onClose={vi.fn()}
|
|
501
|
+
onBackToParent={vi.fn()}
|
|
502
|
+
/>,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
expect(
|
|
506
|
+
screen.getByLabelText("Session has unread updates"),
|
|
507
|
+
).toBeTruthy();
|
|
508
|
+
|
|
509
|
+
rerender(
|
|
510
|
+
<ChatChildSessionPanel
|
|
511
|
+
tabs={[
|
|
512
|
+
{
|
|
513
|
+
sessionKey: "child-session-1",
|
|
514
|
+
parentSessionKey: "parent-session-1",
|
|
515
|
+
label: "北京天气",
|
|
516
|
+
agentId: "weather",
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
sessionKey: "child-session-2",
|
|
520
|
+
parentSessionKey: "parent-session-1",
|
|
521
|
+
label: "上海天气",
|
|
522
|
+
agentId: "weather",
|
|
523
|
+
},
|
|
524
|
+
]}
|
|
525
|
+
activeSessionKey="child-session-2"
|
|
526
|
+
onSelectSession={vi.fn()}
|
|
527
|
+
onClose={vi.fn()}
|
|
528
|
+
onBackToParent={vi.fn()}
|
|
529
|
+
/>,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
expect(
|
|
533
|
+
screen.queryByLabelText("Session has unread updates"),
|
|
534
|
+
).toBeNull();
|
|
333
535
|
});
|
|
334
536
|
});
|