@nextclaw/ui 0.12.23 → 0.12.25
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 +136 -0
- package/dist/assets/api-DGD9_Bg4.js +15 -0
- package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
- package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
- package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
- package/dist/assets/chat-page-D1fMNBrT.js +1 -0
- package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
- package/dist/assets/cpu-DPPwMzoC.js +3 -0
- package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
- package/dist/assets/desktop-kk7qvZ-v.js +3 -0
- package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
- package/dist/assets/{dialog-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
- package/dist/assets/{dist-DsYTOyq7.js → dist-CFiwgaLs.js} +1 -1
- package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
- package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
- package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
- package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
- package/dist/assets/download-CMM8po31.js +1 -0
- package/dist/assets/{es2015-V75WQJ2s.js → es2015-BhznEEyJ.js} +1 -1
- package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
- package/dist/assets/i18n-D1144VAA.js +1 -0
- package/dist/assets/index-D-AAMKCt.js +103 -0
- package/dist/assets/index-DnBeV2Xm.css +1 -0
- package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
- package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
- package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
- package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
- package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
- package/dist/assets/model-config-Bc6VVnxy.js +1 -0
- package/dist/assets/{notice-card-D1RNsTn_.js → notice-card-Dr6xCwva.js} +1 -1
- package/dist/assets/play-AqrNslHI.js +1 -0
- package/dist/assets/plus-B-YHtTNC.js +1 -0
- package/dist/assets/{popover-BMyiifTA.js → popover-BDFNiLlg.js} +1 -1
- package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
- package/dist/assets/providers-list-DN0tvISH.js +1 -0
- package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
- package/dist/assets/remote-Dr3jcfWP.js +1 -0
- package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
- package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
- package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
- package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
- package/dist/assets/search-config-C4c1yZSP.js +1 -0
- package/dist/assets/secrets-config-zAF30YfO.js +3 -0
- package/dist/assets/{select-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
- package/dist/assets/{setting-row-CvKngoNI.js → setting-row-BavcnXw1.js} +1 -1
- package/dist/assets/settings-MWL2SMyk.js +1 -0
- package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
- package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
- package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
- package/dist/assets/{tag-chip-BywQeHJj.js → tag-chip-Dm2Lqnpu.js} +1 -1
- package/dist/assets/use-config-Cyv5IuSt.js +1 -0
- package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
- package/dist/assets/x-BeyYA_h6.js +1 -0
- package/dist/index.html +29 -40
- package/package.json +9 -9
- package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
- package/src/app/components/theme-provider.tsx +1 -0
- package/src/app/configs/app-navigation.config.ts +0 -6
- package/src/app/index.tsx +4 -7
- package/src/features/agents/components/agents-page.test.tsx +25 -15
- package/src/features/agents/components/agents-page.tsx +133 -172
- package/src/features/channels/components/config/channel-form.test.tsx +1 -0
- package/src/features/channels/components/config/channel-form.tsx +4 -3
- package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
- package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
- package/src/features/channels/index.ts +1 -1
- package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
- package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
- package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
- package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
- package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
- package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +56 -25
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
- package/src/features/chat/components/layout/chat-sidebar.test.tsx +119 -8
- package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
- package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
- package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
- package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
- package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
- package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
- package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
- package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
- package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
- package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
- package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
- package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
- package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
- package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
- package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
- package/src/features/chat/stores/chat-session-list.store.ts +0 -3
- package/src/features/chat/stores/chat-thread.store.ts +4 -0
- package/src/features/chat/types/chat-stream.types.ts +1 -1
- package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
- package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
- package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
- package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
- package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
- package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
- package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
- package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
- package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
- package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
- package/src/features/marketplace/components/marketplace-page.tsx +154 -132
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
- package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
- package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
- package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
- package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
- package/src/features/system-status/components/runtime-control-card.tsx +7 -6
- package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
- package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
- package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
- package/src/features/system-status/utils/system-status.utils.ts +31 -6
- package/src/index.css +8 -0
- package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
- package/src/platforms/desktop/index.ts +6 -0
- package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
- package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
- package/src/shared/components/common/brand-header.tsx +36 -16
- package/src/shared/components/config/provider-form-support.ts +2 -22
- package/src/shared/components/cron-config.tsx +12 -58
- package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
- package/src/shared/components/ui/select.tsx +19 -7
- package/src/shared/lib/api/channel-auth.types.ts +1 -0
- package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
- package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
- package/src/shared/lib/api/ncp-session.types.ts +9 -0
- package/src/shared/lib/api/types.ts +12 -1
- package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
- package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
- package/src/shared/lib/cron/index.ts +1 -0
- package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
- package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
- package/src/shared/lib/i18n/index.ts +20 -59
- package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
- package/src/shared/lib/provider-models/index.test.ts +39 -0
- package/src/shared/lib/provider-models/index.ts +1 -3
- package/src/shared/lib/ui-document-title/index.ts +0 -1
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
- package/vitest.config.ts +1 -1
- package/dist/assets/api-BGd3rgv_.js +0 -15
- package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
- package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
- package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
- package/dist/assets/chat-DGM6K3Qs.js +0 -61
- package/dist/assets/chat-page-DpmXMWNS.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
- package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
- package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
- package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
- package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
- package/dist/assets/doc-browser-p82AdNO-.js +0 -1
- package/dist/assets/folder-CeJKPx5P.js +0 -1
- package/dist/assets/hash-BqxRTZW5.js +0 -1
- package/dist/assets/i18n-DnTGDIRw.js +0 -1
- package/dist/assets/index-BrEdR78s.js +0 -2
- package/dist/assets/index-D8MKmXtO.css +0 -1
- package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
- package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
- package/dist/assets/logos-C4sYP1Vl.js +0 -1
- package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
- package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
- package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
- package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
- package/dist/assets/message-square-z_osm9c0.js +0 -1
- package/dist/assets/model-config-Cmruiqdx.js +0 -1
- package/dist/assets/play-Dv6Nr1Ew.js +0 -1
- package/dist/assets/plus-D8eKFY7h.js +0 -1
- package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
- package/dist/assets/providers-list-gg7LrfuB.js +0 -1
- package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
- package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
- package/dist/assets/remote-Db2M39Cv.js +0 -1
- package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
- package/dist/assets/search-config-0VTPpz-w.js +0 -1
- package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
- package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
- package/dist/assets/settings-drbWqzA4.js +0 -1
- package/dist/assets/skeleton-BK1SOSRA.js +0 -1
- package/dist/assets/theme-provider-COAwWFv8.js +0 -2
- package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
- package/dist/assets/trash-2-CBsHCfqq.js +0 -1
- package/dist/assets/use-config-DTwhNDQE.js +0 -1
- package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
- package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
- package/dist/assets/x-CM-XDMpk.js +0 -1
- package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
- package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
- package/src/features/chat/pages/sessions-config-page.tsx +0 -192
- /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
|
@@ -5,9 +5,13 @@ import { SessionRunBadge } from '@/features/chat/components/session/session-run-
|
|
|
5
5
|
import { Button } from '@/shared/components/ui/button';
|
|
6
6
|
import { Input } from '@/shared/components/ui/input';
|
|
7
7
|
import { type SessionContextView } from '@/features/chat/utils/session-context.utils';
|
|
8
|
+
import {
|
|
9
|
+
formatSessionListTime,
|
|
10
|
+
sessionActivityPreviewText
|
|
11
|
+
} from '@/features/chat/utils/chat-session-display.utils';
|
|
8
12
|
import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
|
|
9
13
|
import { cn } from '@/shared/lib/utils';
|
|
10
|
-
import {
|
|
14
|
+
import { t } from '@/shared/lib/i18n';
|
|
11
15
|
import { Check, GitBranch, Pencil, X } from 'lucide-react';
|
|
12
16
|
|
|
13
17
|
type ChatSidebarSessionItemProps = {
|
|
@@ -116,6 +120,8 @@ function ChatSidebarSessionDisplayView({
|
|
|
116
120
|
onStartEditing
|
|
117
121
|
}: ChatSidebarSessionDisplayViewProps) {
|
|
118
122
|
const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-14' : 'pr-6';
|
|
123
|
+
const previewText = sessionActivityPreviewText(session);
|
|
124
|
+
const fallbackPreviewText = `${agentLabel?.trim() ? `${agentLabel} · ` : ''}${session.messageCount}`;
|
|
119
125
|
|
|
120
126
|
return (
|
|
121
127
|
<div className="group/session relative">
|
|
@@ -157,7 +163,7 @@ function ChatSidebarSessionDisplayView({
|
|
|
157
163
|
</div>
|
|
158
164
|
<div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
|
|
159
165
|
<span className="min-w-0 truncate">
|
|
160
|
-
{
|
|
166
|
+
{previewText ?? fallbackPreviewText}
|
|
161
167
|
</span>
|
|
162
168
|
{showUnreadDot ? (
|
|
163
169
|
<span
|
|
@@ -165,7 +171,7 @@ function ChatSidebarSessionDisplayView({
|
|
|
165
171
|
className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
|
|
166
172
|
/>
|
|
167
173
|
) : (
|
|
168
|
-
<span className="ml-auto shrink-0">{
|
|
174
|
+
<span className="ml-auto shrink-0">{formatSessionListTime(session.lastMessageAt ?? session.createdAt)}</span>
|
|
169
175
|
)}
|
|
170
176
|
</div>
|
|
171
177
|
</button>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ChatConversationHeader } from "@/features/chat/components/conversation/chat-conversation-header";
|
|
5
|
+
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
6
|
+
|
|
7
|
+
vi.mock("@/shared/components/common/agent-avatar", () => ({
|
|
8
|
+
AgentAvatar: ({ agentId }: { agentId: string }) => (
|
|
9
|
+
<div data-testid="agent-avatar">{agentId}</div>
|
|
10
|
+
),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
function renderHeader(
|
|
14
|
+
snapshotPatch: Partial<ReturnType<typeof useChatThreadStore.getState>["snapshot"]>,
|
|
15
|
+
) {
|
|
16
|
+
const queryClient = new QueryClient();
|
|
17
|
+
const snapshot = {
|
|
18
|
+
...useChatThreadStore.getState().snapshot,
|
|
19
|
+
isProviderStateResolved: true,
|
|
20
|
+
modelOptions: [],
|
|
21
|
+
sessionTypeLabel: "Codex",
|
|
22
|
+
sessionKey: null,
|
|
23
|
+
agentId: "main",
|
|
24
|
+
canDeleteSession: false,
|
|
25
|
+
messages: [],
|
|
26
|
+
...snapshotPatch,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return render(
|
|
30
|
+
<QueryClientProvider client={queryClient}>
|
|
31
|
+
<ChatConversationHeader
|
|
32
|
+
snapshot={snapshot}
|
|
33
|
+
childSessionCount={0}
|
|
34
|
+
sessionCronJobCount={0}
|
|
35
|
+
layoutMode="desktop"
|
|
36
|
+
normalizedAgentId={snapshot.agentId ?? ""}
|
|
37
|
+
sessionHeaderTitle={snapshot.sessionDisplayName ?? "New Task"}
|
|
38
|
+
shouldShowHeaderAgentAvatar={false}
|
|
39
|
+
shouldShowSessionHeader={Boolean(
|
|
40
|
+
snapshot.sessionKey || snapshot.sessionTypeLabel,
|
|
41
|
+
)}
|
|
42
|
+
onOpenChildSessions={vi.fn()}
|
|
43
|
+
onOpenSessionCronJobs={vi.fn()}
|
|
44
|
+
onDeleteSession={vi.fn()}
|
|
45
|
+
/>
|
|
46
|
+
</QueryClientProvider>,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("ChatConversationHeader", () => {
|
|
51
|
+
it("does not reserve extra height for draft sessions", () => {
|
|
52
|
+
renderHeader({});
|
|
53
|
+
|
|
54
|
+
const header = screen.getByText("New Task").closest(".border-b");
|
|
55
|
+
|
|
56
|
+
expect(header?.className).not.toContain("min-h-");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("uses the standard session-header action button density after the session is materialized", () => {
|
|
60
|
+
renderHeader({
|
|
61
|
+
sessionKey: "session-1",
|
|
62
|
+
canDeleteSession: true,
|
|
63
|
+
sessionDisplayName: "First message",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const moreActions = screen.getByRole("button", { name: "More actions" });
|
|
67
|
+
|
|
68
|
+
expect(moreActions.className).toContain("h-7");
|
|
69
|
+
expect(moreActions.className).toContain("w-7");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -44,6 +44,7 @@ export function ChatParentSessionBanner({
|
|
|
44
44
|
export function ChatConversationHeader({
|
|
45
45
|
snapshot,
|
|
46
46
|
childSessionCount,
|
|
47
|
+
sessionCronJobCount,
|
|
47
48
|
layoutMode,
|
|
48
49
|
normalizedAgentId,
|
|
49
50
|
sessionHeaderTitle,
|
|
@@ -51,10 +52,12 @@ export function ChatConversationHeader({
|
|
|
51
52
|
shouldShowSessionHeader,
|
|
52
53
|
onBackToList,
|
|
53
54
|
onOpenChildSessions,
|
|
55
|
+
onOpenSessionCronJobs,
|
|
54
56
|
onDeleteSession,
|
|
55
57
|
}: {
|
|
56
58
|
snapshot: ChatThreadSnapshot;
|
|
57
59
|
childSessionCount: number;
|
|
60
|
+
sessionCronJobCount: number;
|
|
58
61
|
layoutMode: "desktop" | "mobile";
|
|
59
62
|
normalizedAgentId: string;
|
|
60
63
|
sessionHeaderTitle: string;
|
|
@@ -62,6 +65,7 @@ export function ChatConversationHeader({
|
|
|
62
65
|
shouldShowSessionHeader: boolean;
|
|
63
66
|
onBackToList?: () => void;
|
|
64
67
|
onOpenChildSessions: () => void;
|
|
68
|
+
onOpenSessionCronJobs: () => void;
|
|
65
69
|
onDeleteSession: ChatHeaderDeleteHandler;
|
|
66
70
|
}) {
|
|
67
71
|
const isMobileLayout = layoutMode === "mobile";
|
|
@@ -137,7 +141,9 @@ export function ChatConversationHeader({
|
|
|
137
141
|
isDeletePending={snapshot.isDeletePending}
|
|
138
142
|
projectRoot={snapshot.sessionProjectRoot}
|
|
139
143
|
childSessionCount={childSessionCount}
|
|
144
|
+
sessionCronJobCount={sessionCronJobCount}
|
|
140
145
|
onOpenChildSessions={onOpenChildSessions}
|
|
146
|
+
onOpenSessionCronJobs={onOpenSessionCronJobs}
|
|
141
147
|
onDeleteSession={onDeleteSession}
|
|
142
148
|
/>
|
|
143
149
|
) : null}
|
|
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
import { ChatConversationPanel } from "@/features/chat/components/conversation/chat-conversation-panel";
|
|
5
5
|
import { ChatSessionWorkspacePanel } from "@/features/chat";
|
|
6
6
|
import type { ResolvedChildSessionTab } from "@/features/chat/hooks/use-ncp-child-session-tabs-view";
|
|
7
|
+
import type { CronJobView } from "@/shared/lib/api";
|
|
7
8
|
import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
|
|
8
9
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
9
10
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
@@ -12,7 +13,11 @@ const mocks = vi.hoisted(() => ({
|
|
|
12
13
|
deleteSession: vi.fn(),
|
|
13
14
|
goToProviders: vi.fn(),
|
|
14
15
|
createSession: vi.fn(() => "draft-session-2"),
|
|
16
|
+
goToChatRoot: vi.fn(),
|
|
15
17
|
goToSession: vi.fn(),
|
|
18
|
+
openSessionCronPanel: vi.fn(),
|
|
19
|
+
deleteCronJob: vi.fn(),
|
|
20
|
+
cronJobs: [] as CronJobView[],
|
|
16
21
|
setSelectedAgentId: vi.fn(),
|
|
17
22
|
setPendingSessionType: vi.fn(),
|
|
18
23
|
stickyBottomScroll: vi.fn(() => ({
|
|
@@ -48,7 +53,19 @@ vi.mock("@/features/chat/components/conversation/chat-input-bar.container", () =
|
|
|
48
53
|
}));
|
|
49
54
|
|
|
50
55
|
vi.mock("@/features/chat/components/conversation/chat-message-list.container", () => ({
|
|
51
|
-
ChatMessageListContainer: (
|
|
56
|
+
ChatMessageListContainer: ({
|
|
57
|
+
isSending,
|
|
58
|
+
messages,
|
|
59
|
+
}: {
|
|
60
|
+
isSending: boolean;
|
|
61
|
+
messages: readonly unknown[];
|
|
62
|
+
}) => (
|
|
63
|
+
<div
|
|
64
|
+
data-testid="chat-message-list"
|
|
65
|
+
data-message-count={String(messages.length)}
|
|
66
|
+
data-sending={String(isSending)}
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
52
69
|
}));
|
|
53
70
|
|
|
54
71
|
vi.mock("@/features/chat/components/chat-session-workspace-file-preview", () => ({
|
|
@@ -81,12 +98,14 @@ vi.mock("@/features/chat/components/chat-welcome", () => ({
|
|
|
81
98
|
vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
82
99
|
usePresenter: () => ({
|
|
83
100
|
chatUiManager: {
|
|
101
|
+
goToChatRoot: mocks.goToChatRoot,
|
|
84
102
|
goToSession: mocks.goToSession,
|
|
85
103
|
},
|
|
86
104
|
chatThreadManager: {
|
|
87
105
|
deleteSession: mocks.deleteSession,
|
|
88
106
|
goToProviders: mocks.goToProviders,
|
|
89
107
|
openChildSessionPanel: vi.fn(),
|
|
108
|
+
openSessionCronPanel: mocks.openSessionCronPanel,
|
|
90
109
|
openFilePreview: vi.fn(),
|
|
91
110
|
openSessionFromToolAction: vi.fn(),
|
|
92
111
|
selectChildSessionDetail: vi.fn(),
|
|
@@ -116,6 +135,14 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
116
135
|
}),
|
|
117
136
|
}));
|
|
118
137
|
|
|
138
|
+
vi.mock("@/shared/hooks/use-config", () => ({
|
|
139
|
+
useCronJobs: () => ({ data: { jobs: mocks.cronJobs, total: mocks.cronJobs.length } }),
|
|
140
|
+
useDeleteCronJob: () => ({
|
|
141
|
+
mutate: mocks.deleteCronJob,
|
|
142
|
+
isPending: false,
|
|
143
|
+
}),
|
|
144
|
+
}));
|
|
145
|
+
|
|
119
146
|
vi.mock("@/features/chat/components/conversation/session-header/chat-session-header-actions", () => ({
|
|
120
147
|
ChatSessionHeaderActions: () => <button aria-label="More actions" />,
|
|
121
148
|
}));
|
|
@@ -157,66 +184,73 @@ vi.mock("@/shared/components/common/agent-identity", () => ({
|
|
|
157
184
|
),
|
|
158
185
|
}));
|
|
159
186
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
187
|
+
function resetChatConversationPanelTestState() {
|
|
188
|
+
mocks.deleteSession.mockReset();
|
|
189
|
+
mocks.goToProviders.mockReset();
|
|
190
|
+
mocks.createSession.mockReset();
|
|
191
|
+
mocks.createSession.mockReturnValue("draft-session-2");
|
|
192
|
+
mocks.goToChatRoot.mockReset();
|
|
193
|
+
mocks.goToSession.mockReset();
|
|
194
|
+
mocks.openSessionCronPanel.mockReset();
|
|
195
|
+
mocks.deleteCronJob.mockReset();
|
|
196
|
+
mocks.cronJobs = [];
|
|
197
|
+
mocks.setSelectedAgentId.mockReset();
|
|
198
|
+
mocks.setPendingSessionType.mockReset();
|
|
199
|
+
mocks.stickyBottomScroll.mockClear();
|
|
200
|
+
useChatInputStore.setState({
|
|
201
|
+
snapshot: {
|
|
202
|
+
...useChatInputStore.getState().snapshot,
|
|
203
|
+
defaultSessionType: "native",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
useChatThreadStore.setState({
|
|
207
|
+
snapshot: {
|
|
208
|
+
...useChatThreadStore.getState().snapshot,
|
|
209
|
+
isProviderStateResolved: true,
|
|
210
|
+
modelOptions: [
|
|
211
|
+
{
|
|
212
|
+
value: "openai/gpt-5.1",
|
|
213
|
+
modelLabel: "gpt-5.1",
|
|
214
|
+
providerLabel: "OpenAI",
|
|
215
|
+
} as never,
|
|
216
|
+
],
|
|
217
|
+
sessionTypeLabel: "Codex",
|
|
218
|
+
sessionKey: "draft-session-1",
|
|
219
|
+
sessionDisplayName: undefined,
|
|
220
|
+
agentId: null,
|
|
221
|
+
agentDisplayName: null,
|
|
222
|
+
sessionProjectRoot: null,
|
|
223
|
+
sessionProjectName: null,
|
|
224
|
+
canDeleteSession: false,
|
|
225
|
+
isDeletePending: false,
|
|
226
|
+
isHistoryLoading: false,
|
|
227
|
+
messages: [],
|
|
228
|
+
isSending: false,
|
|
229
|
+
isAwaitingAssistantOutput: false,
|
|
230
|
+
hasSubmittedDraftMessage: false,
|
|
231
|
+
parentSessionKey: null,
|
|
232
|
+
parentSessionLabel: null,
|
|
233
|
+
workspacePanelParentKey: null,
|
|
234
|
+
availableAgents: [
|
|
235
|
+
{ id: "main", displayName: "Main", runtime: "native" },
|
|
236
|
+
{ id: "engineer", displayName: "Engineer", runtime: "codex" },
|
|
237
|
+
],
|
|
238
|
+
childSessionTabs: [],
|
|
239
|
+
activeChildSessionKey: null,
|
|
240
|
+
workspaceFileTabs: [],
|
|
241
|
+
activeWorkspaceFileKey: null,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
useChatSessionListStore.setState({
|
|
245
|
+
optimisticReadAtBySessionKey: {},
|
|
246
|
+
snapshot: {
|
|
247
|
+
...useChatSessionListStore.getState().snapshot,
|
|
248
|
+
},
|
|
219
249
|
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
describe("ChatConversationPanel", () => {
|
|
253
|
+
beforeEach(resetChatConversationPanelTestState);
|
|
220
254
|
|
|
221
255
|
it("shows the draft session type in the conversation header", () => {
|
|
222
256
|
render(<ChatConversationPanel />);
|
|
@@ -247,7 +281,7 @@ describe("ChatConversationPanel", () => {
|
|
|
247
281
|
await user.click(screen.getByRole("button", { name: "create draft session" }));
|
|
248
282
|
|
|
249
283
|
expect(mocks.createSession).toHaveBeenCalledWith("native");
|
|
250
|
-
expect(mocks.
|
|
284
|
+
expect(mocks.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
251
285
|
});
|
|
252
286
|
|
|
253
287
|
it("shows the selected session project badge and more actions trigger", () => {
|
|
@@ -334,6 +368,40 @@ describe("ChatConversationPanel", () => {
|
|
|
334
368
|
expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
|
|
335
369
|
});
|
|
336
370
|
|
|
371
|
+
it("keeps the message list mounted while waiting for the first assistant token", () => {
|
|
372
|
+
useChatThreadStore.setState({
|
|
373
|
+
snapshot: {
|
|
374
|
+
...useChatThreadStore.getState().snapshot,
|
|
375
|
+
messages: [],
|
|
376
|
+
isSending: true,
|
|
377
|
+
isAwaitingAssistantOutput: true,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
render(<ChatConversationPanel />);
|
|
382
|
+
|
|
383
|
+
expect(screen.getByTestId("chat-message-list").dataset).toMatchObject({ messageCount: "0", sending: "true" });
|
|
384
|
+
expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("does not reopen the welcome panel after a root draft send fails", () => {
|
|
388
|
+
useChatThreadStore.setState({
|
|
389
|
+
snapshot: {
|
|
390
|
+
...useChatThreadStore.getState().snapshot,
|
|
391
|
+
sessionKey: null,
|
|
392
|
+
messages: [],
|
|
393
|
+
isSending: false,
|
|
394
|
+
isAwaitingAssistantOutput: false,
|
|
395
|
+
hasSubmittedDraftMessage: true,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
render(<ChatConversationPanel />);
|
|
400
|
+
|
|
401
|
+
expect(screen.queryByTestId("chat-welcome")).toBeNull();
|
|
402
|
+
expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
|
|
337
405
|
it("does not render runtime lifecycle copy in the conversation alert strip", () => {
|
|
338
406
|
render(<ChatConversationPanel />);
|
|
339
407
|
|
|
@@ -555,4 +623,56 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
555
623
|
"workspace-horizontal-scrollbar",
|
|
556
624
|
);
|
|
557
625
|
});
|
|
626
|
+
|
|
627
|
+
it("renders session cron jobs in the workspace sidebar and deletes with a neutral confirmation", async () => {
|
|
628
|
+
const user = userEvent.setup();
|
|
629
|
+
const job: CronJobView = {
|
|
630
|
+
id: "job-1",
|
|
631
|
+
name: "Follow up",
|
|
632
|
+
enabled: true,
|
|
633
|
+
schedule: { kind: "every", everyMs: 3600000 },
|
|
634
|
+
payload: {
|
|
635
|
+
kind: "agent_turn",
|
|
636
|
+
message: "Continue this session later",
|
|
637
|
+
sessionId: "parent-session-1",
|
|
638
|
+
},
|
|
639
|
+
state: {
|
|
640
|
+
nextRunAt: "2026-05-15T10:00:00.000Z",
|
|
641
|
+
lastRunAt: null,
|
|
642
|
+
lastStatus: null,
|
|
643
|
+
lastError: null,
|
|
644
|
+
},
|
|
645
|
+
createdAt: "2026-05-15T09:00:00.000Z",
|
|
646
|
+
updatedAt: "2026-05-15T09:00:00.000Z",
|
|
647
|
+
deleteAfterRun: false,
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
render(
|
|
651
|
+
<ChatSessionWorkspacePanel
|
|
652
|
+
childSessionTabs={[]}
|
|
653
|
+
activeChildSessionKey={null}
|
|
654
|
+
workspaceFileTabs={[]}
|
|
655
|
+
activeWorkspaceFileKey={null}
|
|
656
|
+
activePanelKind="cron"
|
|
657
|
+
sessionCronJobs={[job]}
|
|
658
|
+
sessionProjectRoot="/Users/demo/project-alpha"
|
|
659
|
+
onSelectSession={vi.fn()}
|
|
660
|
+
onSelectFile={vi.fn()}
|
|
661
|
+
onCloseFile={vi.fn()}
|
|
662
|
+
onClose={vi.fn()}
|
|
663
|
+
onBackToParent={vi.fn()}
|
|
664
|
+
onFileOpen={vi.fn()}
|
|
665
|
+
/>,
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
expect(screen.getAllByText("Session cron jobs").length).toBeGreaterThan(0);
|
|
669
|
+
expect(screen.getByText("Follow up")).toBeTruthy();
|
|
670
|
+
expect(screen.getByText("Continue this session later")).toBeTruthy();
|
|
671
|
+
|
|
672
|
+
await user.click(screen.getByRole("button", { name: "Delete" }));
|
|
673
|
+
expect(screen.getByText("Delete cron job?")).toBeTruthy();
|
|
674
|
+
await user.click(screen.getAllByRole("button", { name: "Delete" }).at(-1)!);
|
|
675
|
+
|
|
676
|
+
expect(mocks.deleteCronJob).toHaveBeenCalledWith({ id: "job-1" });
|
|
677
|
+
});
|
|
558
678
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComponentProps, useRef } from "react";
|
|
1
|
+
import { type ComponentProps, useMemo, useRef } from "react";
|
|
2
2
|
import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
|
|
3
3
|
import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
|
|
4
4
|
import { ChatInputBarContainer } from "@/features/chat/components/conversation/chat-input-bar.container";
|
|
@@ -13,6 +13,8 @@ import { usePresenter } from "@/features/chat/components/providers/chat-presente
|
|
|
13
13
|
import { resolveAgentRuntimeSessionType } from "@/features/chat/hooks/use-chat-session-type-state";
|
|
14
14
|
import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
|
|
15
15
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
16
|
+
import { useCronJobs } from "@/shared/hooks/use-config";
|
|
17
|
+
import { isCronJobForSession } from "@/shared/lib/cron";
|
|
16
18
|
import { Skeleton } from "@/shared/components/ui/skeleton";
|
|
17
19
|
import { t } from "@/shared/lib/i18n";
|
|
18
20
|
import { cn } from "@/shared/lib/utils";
|
|
@@ -173,6 +175,11 @@ function ChatConversationContent({
|
|
|
173
175
|
onToolAction,
|
|
174
176
|
onFileOpen,
|
|
175
177
|
}: ChatConversationContentProps) {
|
|
178
|
+
const isAwaitingAssistantOutput =
|
|
179
|
+
snapshot.isSending && snapshot.isAwaitingAssistantOutput;
|
|
180
|
+
const shouldShowMessages =
|
|
181
|
+
snapshot.messages.length > 0 || isAwaitingAssistantOutput;
|
|
182
|
+
|
|
176
183
|
return (
|
|
177
184
|
<div
|
|
178
185
|
ref={threadRef}
|
|
@@ -186,16 +193,11 @@ function ChatConversationContent({
|
|
|
186
193
|
selectedAgentId={snapshot.agentId ?? "main"}
|
|
187
194
|
onSelectAgent={onSelectAgent}
|
|
188
195
|
/>
|
|
189
|
-
) : hideEmptyHint ? null :
|
|
190
|
-
<div className="px-4 py-4 text-sm text-gray-500 sm:px-5 sm:py-5">
|
|
191
|
-
{t("chatNoMessages")}
|
|
192
|
-
</div>
|
|
193
|
-
) : (
|
|
196
|
+
) : hideEmptyHint || !shouldShowMessages ? null : (
|
|
194
197
|
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
|
|
195
198
|
<ChatMessageListContainer
|
|
196
|
-
key={snapshot.sessionKey ?? "draft"}
|
|
197
199
|
messages={snapshot.messages}
|
|
198
|
-
isSending={
|
|
200
|
+
isSending={isAwaitingAssistantOutput}
|
|
199
201
|
onToolAction={onToolAction}
|
|
200
202
|
onFileOpen={onFileOpen}
|
|
201
203
|
/>
|
|
@@ -209,11 +211,39 @@ function shouldShowWorkspacePanel(
|
|
|
209
211
|
snapshot: ChatThreadSnapshot,
|
|
210
212
|
childSessionTabs: ChatThreadSnapshot["childSessionTabs"],
|
|
211
213
|
workspaceFileTabs: ChatThreadSnapshot["workspaceFileTabs"],
|
|
214
|
+
sessionCronJobCount: number,
|
|
212
215
|
) {
|
|
213
216
|
if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
|
|
214
217
|
return false;
|
|
215
218
|
}
|
|
216
|
-
return childSessionTabs.length > 0 || workspaceFileTabs.length > 0;
|
|
219
|
+
return childSessionTabs.length > 0 || workspaceFileTabs.length > 0 || sessionCronJobCount > 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function useSessionWorkspaceState(snapshot: ChatThreadSnapshot) {
|
|
223
|
+
const childSessionTabs = useMemo(
|
|
224
|
+
() => snapshot.childSessionTabs.filter((tab) => tab.parentSessionKey === snapshot.sessionKey),
|
|
225
|
+
[snapshot.childSessionTabs, snapshot.sessionKey],
|
|
226
|
+
);
|
|
227
|
+
const workspaceFileTabs = useMemo(
|
|
228
|
+
() => snapshot.workspaceFileTabs.filter((tab) => tab.parentSessionKey === snapshot.sessionKey),
|
|
229
|
+
[snapshot.sessionKey, snapshot.workspaceFileTabs],
|
|
230
|
+
);
|
|
231
|
+
const cronQuery = useCronJobs({ all: true });
|
|
232
|
+
const sessionCronJobs = useMemo(
|
|
233
|
+
() => (cronQuery.data?.jobs ?? []).filter((job) => isCronJobForSession(job, snapshot.sessionKey)),
|
|
234
|
+
[cronQuery.data?.jobs, snapshot.sessionKey],
|
|
235
|
+
);
|
|
236
|
+
return {
|
|
237
|
+
childSessionTabs,
|
|
238
|
+
workspaceFileTabs,
|
|
239
|
+
sessionCronJobs,
|
|
240
|
+
showWorkspacePanel: shouldShowWorkspacePanel(
|
|
241
|
+
snapshot,
|
|
242
|
+
childSessionTabs,
|
|
243
|
+
workspaceFileTabs,
|
|
244
|
+
sessionCronJobs.length,
|
|
245
|
+
),
|
|
246
|
+
};
|
|
217
247
|
}
|
|
218
248
|
|
|
219
249
|
export function ChatConversationPanel({
|
|
@@ -224,23 +254,12 @@ export function ChatConversationPanel({
|
|
|
224
254
|
onBackToList?: () => void;
|
|
225
255
|
}) {
|
|
226
256
|
const presenter = usePresenter();
|
|
227
|
-
const defaultSessionType = useChatInputStore(
|
|
228
|
-
(state) => state.snapshot.defaultSessionType,
|
|
229
|
-
);
|
|
257
|
+
const defaultSessionType = useChatInputStore((state) => state.snapshot.defaultSessionType);
|
|
230
258
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
231
259
|
const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
|
|
232
260
|
const threadRef = snapshot.threadRef ?? fallbackThreadRef;
|
|
233
|
-
const childSessionTabs =
|
|
234
|
-
(
|
|
235
|
-
);
|
|
236
|
-
const workspaceFileTabs = snapshot.workspaceFileTabs.filter(
|
|
237
|
-
(tab) => tab.parentSessionKey === snapshot.sessionKey,
|
|
238
|
-
);
|
|
239
|
-
const showWorkspacePanel = shouldShowWorkspacePanel(
|
|
240
|
-
snapshot,
|
|
241
|
-
childSessionTabs,
|
|
242
|
-
workspaceFileTabs,
|
|
243
|
-
);
|
|
261
|
+
const { childSessionTabs, workspaceFileTabs, sessionCronJobs, showWorkspacePanel } =
|
|
262
|
+
useSessionWorkspaceState(snapshot);
|
|
244
263
|
const shouldShowSessionHeader = Boolean(
|
|
245
264
|
snapshot.sessionKey || snapshot.sessionTypeLabel,
|
|
246
265
|
);
|
|
@@ -255,6 +274,7 @@ export function ChatConversationPanel({
|
|
|
255
274
|
|
|
256
275
|
const showWelcome =
|
|
257
276
|
!snapshot.canDeleteSession &&
|
|
277
|
+
!snapshot.hasSubmittedDraftMessage &&
|
|
258
278
|
snapshot.messages.length === 0 &&
|
|
259
279
|
!snapshot.isSending;
|
|
260
280
|
const hasConfiguredModel = snapshot.modelOptions.length > 0;
|
|
@@ -273,8 +293,8 @@ export function ChatConversationPanel({
|
|
|
273
293
|
resolveDraftAgent(snapshot.agentId ?? "main"),
|
|
274
294
|
defaultSessionType,
|
|
275
295
|
);
|
|
276
|
-
|
|
277
|
-
if (layoutMode === "mobile") presenter.chatUiManager.
|
|
296
|
+
presenter.chatSessionListManager.createSession(sessionType);
|
|
297
|
+
if (layoutMode === "mobile") presenter.chatUiManager.goToChatRoot();
|
|
278
298
|
};
|
|
279
299
|
const selectDraftAgent = (agentId: string) => {
|
|
280
300
|
presenter.chatSessionListManager.setSelectedAgentId(agentId);
|
|
@@ -294,6 +314,12 @@ export function ChatConversationPanel({
|
|
|
294
314
|
activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
|
|
295
315
|
});
|
|
296
316
|
};
|
|
317
|
+
const openSessionCronJobs = () => {
|
|
318
|
+
if (!snapshot.sessionKey || sessionCronJobs.length === 0) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey);
|
|
322
|
+
};
|
|
297
323
|
|
|
298
324
|
const { onScroll: handleScroll } = useStickyBottomScroll({
|
|
299
325
|
scrollRef: threadRef,
|
|
@@ -319,6 +345,7 @@ export function ChatConversationPanel({
|
|
|
319
345
|
<ChatConversationHeader
|
|
320
346
|
snapshot={snapshot}
|
|
321
347
|
childSessionCount={childSessionTabs.length}
|
|
348
|
+
sessionCronJobCount={sessionCronJobs.length}
|
|
322
349
|
layoutMode={layoutMode}
|
|
323
350
|
normalizedAgentId={normalizedAgentId}
|
|
324
351
|
sessionHeaderTitle={sessionHeaderTitle}
|
|
@@ -326,6 +353,7 @@ export function ChatConversationPanel({
|
|
|
326
353
|
shouldShowSessionHeader={shouldShowSessionHeader}
|
|
327
354
|
onBackToList={onBackToList}
|
|
328
355
|
onOpenChildSessions={openChildSessions}
|
|
356
|
+
onOpenSessionCronJobs={openSessionCronJobs}
|
|
329
357
|
onDeleteSession={presenter.chatThreadManager.deleteSession}
|
|
330
358
|
/>
|
|
331
359
|
<ChatConversationAlerts
|
|
@@ -356,11 +384,14 @@ export function ChatConversationPanel({
|
|
|
356
384
|
activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
|
|
357
385
|
workspaceFileTabs={workspaceFileTabs}
|
|
358
386
|
activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
|
|
387
|
+
activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
|
|
388
|
+
sessionCronJobs={sessionCronJobs}
|
|
359
389
|
sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
|
|
360
390
|
displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
|
|
361
391
|
onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
|
|
362
392
|
onSelectFile={presenter.chatThreadManager.selectWorkspaceFile}
|
|
363
393
|
onCloseFile={presenter.chatThreadManager.closeWorkspaceFile}
|
|
394
|
+
onSelectCronJobs={() => snapshot.sessionKey ? presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey) : undefined}
|
|
364
395
|
onClose={presenter.chatThreadManager.closeWorkspacePanel}
|
|
365
396
|
onBackToParent={presenter.chatThreadManager.goToParentSession}
|
|
366
397
|
onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
|
|
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
7
7
|
updateSessionProject: vi.fn(),
|
|
8
8
|
onDeleteSession: vi.fn(),
|
|
9
9
|
onOpenChildSessions: vi.fn(),
|
|
10
|
+
onOpenSessionCronJobs: vi.fn(),
|
|
10
11
|
}));
|
|
11
12
|
|
|
12
13
|
vi.mock('@/features/chat/hooks/use-chat-session-project', () => ({
|
|
@@ -22,6 +23,7 @@ describe('ChatSessionHeaderActions', () => {
|
|
|
22
23
|
mocks.updateSessionProject.mockReset();
|
|
23
24
|
mocks.onDeleteSession.mockReset();
|
|
24
25
|
mocks.onOpenChildSessions.mockReset();
|
|
26
|
+
mocks.onOpenSessionCronJobs.mockReset();
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it('keeps only the set-project action in the more-actions menu when a project is already attached', async () => {
|
|
@@ -86,4 +88,26 @@ describe('ChatSessionHeaderActions', () => {
|
|
|
86
88
|
|
|
87
89
|
expect(mocks.onOpenChildSessions).toHaveBeenCalledTimes(1);
|
|
88
90
|
});
|
|
91
|
+
|
|
92
|
+
it('uses a shared spaced action group for child, cron, and menu buttons', () => {
|
|
93
|
+
render(
|
|
94
|
+
<ChatSessionHeaderActions
|
|
95
|
+
sessionKey="session-actions"
|
|
96
|
+
canDeleteSession
|
|
97
|
+
isDeletePending={false}
|
|
98
|
+
projectRoot={null}
|
|
99
|
+
childSessionCount={1}
|
|
100
|
+
sessionCronJobCount={1}
|
|
101
|
+
onOpenChildSessions={mocks.onOpenChildSessions}
|
|
102
|
+
onOpenSessionCronJobs={mocks.onOpenSessionCronJobs}
|
|
103
|
+
onDeleteSession={mocks.onDeleteSession}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const actionGroup = screen.getByRole('button', { name: 'More actions' }).parentElement;
|
|
108
|
+
|
|
109
|
+
expect(actionGroup?.className).toContain('gap-1.5');
|
|
110
|
+
expect(screen.getByRole('button', { name: 'View child sessions' }).className).toContain('h-7');
|
|
111
|
+
expect(screen.getByRole('button', { name: 'View session cron jobs' }).className).toContain('w-7');
|
|
112
|
+
});
|
|
89
113
|
});
|