@nextclaw/ui 0.14.1 → 0.14.3
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 +41 -0
- package/dist/assets/{api-DPcvCLfa.js → api-BP_8IaxB.js} +7 -7
- package/dist/assets/channels-list-page-DEh5Jjqx.js +8 -0
- package/dist/assets/chat-page-BRB0N5WW.js +105 -0
- package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
- package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-DsivFJ4r.js} +2 -2
- package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-B0w9p2ep.js} +1 -1
- package/dist/assets/{dist-DPrgcEC0.js → dist-CikkhOlq.js} +1 -1
- package/dist/assets/{dist-DsE4OsSS.js → dist-PVA_xL1D.js} +1 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
- package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
- package/dist/assets/doc-browser-context-MTw4EvKd.js +1 -0
- package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-y6vDtq4M.js} +1 -1
- package/dist/assets/{external-link-C_dmG-WJ.js → external-link-DVJIQf_G.js} +1 -1
- package/dist/assets/index-BiE-K8-q.css +1 -0
- package/dist/assets/index-D9BAE_L_.js +105 -0
- package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +40 -0
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
- package/dist/assets/middleware-CkFT3R4v.js +1 -0
- package/dist/assets/model-config-page-DotaTUHb.js +1 -0
- package/dist/assets/plus-BbZqquJe.js +1 -0
- package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +1 -0
- package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
- package/dist/assets/{react-sZz5HSCm.js → react-VNqT5Zp7.js} +1 -1
- package/dist/assets/remote-B8TiUfwW.js +1 -0
- package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
- package/dist/assets/runtime-config-page-S6ErRHR8.js +1 -0
- package/dist/assets/{save-MzjoiHri.js → save-BOJ477H-.js} +1 -1
- package/dist/assets/{search-B29dE4GN.js → search-BEti5beA.js} +1 -1
- package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-DGj5am03.js} +1 -1
- package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page--IzrIOid.js} +2 -2
- package/dist/assets/{select-BKmh05hd.js → select-hoj8dK4K.js} +2 -2
- package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-T8-h-die.js} +1 -1
- package/dist/assets/use-config-CjRV2hwL.js +1 -0
- package/dist/assets/x-D9PXs_xP.js +1 -0
- package/dist/index.html +70 -19
- package/index.html +55 -5
- package/package.json +9 -9
- package/src/app/components/app-presenter-provider.tsx +11 -2
- package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
- package/src/app/components/layout/app-layout.tsx +18 -4
- package/src/app/components/layout/header.tsx +3 -3
- package/src/app/components/layout/page-layout.tsx +2 -2
- package/src/app/components/layout/runtime-status-entry.tsx +7 -7
- package/src/app/components/layout/sidebar-items.tsx +194 -71
- package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
- package/src/app/components/layout/sidebar.tsx +340 -83
- package/src/app/managers/viewport-layout.manager.ts +10 -0
- package/src/app/stores/viewport-layout.store.ts +39 -3
- package/src/app/styles/design-system.css +388 -26
- package/src/app/test/vitest-setup.ts +31 -0
- package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
- package/src/features/agents/components/agents-page.tsx +10 -6
- package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
- package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +15 -16
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +19 -19
- package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +18 -16
- package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +93 -20
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +93 -36
- package/src/features/chat/components/layout/chat-sidebar.tsx +60 -93
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +308 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +439 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +194 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
- package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +51 -20
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
- package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
- package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
- package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +99 -18
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
- package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +7 -7
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +6 -6
- package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
- package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
- package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
- package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
- package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
- package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
- package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -9
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +5 -5
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +6 -6
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
- package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
- package/src/features/chat/index.ts +0 -1
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
- package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
- package/src/features/chat/managers/chat-thread.manager.ts +108 -34
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
- package/src/features/chat/presenters/chat.presenter.ts +2 -19
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
- package/src/features/chat/stores/chat-thread.store.ts +42 -26
- package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
- package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
- package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
- package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
- package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
- package/src/features/settings/components/config/provider-form.tsx +10 -10
- package/src/features/settings/components/config/provider-models-section.tsx +16 -16
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/pages/providers-config-page.tsx +17 -17
- package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
- package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
- package/src/shared/components/config-split-page.tsx +11 -11
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
- package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
- package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
- package/src/shared/components/ui/button.tsx +4 -4
- package/src/shared/components/ui/card.tsx +4 -4
- package/src/shared/components/ui/dialog.tsx +5 -5
- package/src/shared/components/ui/input.tsx +1 -1
- package/src/shared/components/ui/label.tsx +1 -1
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +3 -3
- package/src/shared/components/ui/switch.tsx +3 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
- package/src/shared/components/ui/tabs.tsx +4 -4
- package/src/shared/components/ui/textarea.tsx +1 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +9 -2
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +9 -2
- package/src/shared/lib/theme/index.ts +94 -52
- package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
- package/src/shared/lib/ui-document-title/index.ts +50 -21
- package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
- package/dist/assets/chat-page-BBnNJoKG.js +0 -105
- package/dist/assets/config-split-page-BCJhqdK9.js +0 -1
- package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
- package/dist/assets/doc-browser-DgQedeAn.js +0 -1
- package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
- package/dist/assets/index-7o1WdUbU.js +0 -103
- package/dist/assets/index-DQWmEWzk.css +0 -1
- package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
- package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
- package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
- package/dist/assets/plus-Bnt56oqG.js +0 -1
- package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
- package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
- package/dist/assets/remote-B0LMs8u7.js +0 -1
- package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
- package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
- package/dist/assets/use-config-DZkBWIdL.js +0 -1
- package/dist/assets/x-CW2KO-Hz.js +0 -1
- package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
- package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
- package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
- package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
- package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
- package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
- package/src/features/chat/managers/chat-input.manager.ts +0 -430
- package/src/features/chat/managers/chat-run.manager.ts +0 -110
- package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
- package/src/features/chat/stores/chat-input.store.ts +0 -104
- /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { PersistStorage, StorageValue } from "zustand/middleware";
|
|
4
|
+
import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
|
|
5
|
+
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
6
|
+
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
7
|
+
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({
|
|
10
|
+
panelProps: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/features/chat/features/workspace/components/chat-session-workspace-panel", () => ({
|
|
14
|
+
ChatSessionWorkspacePanel: ({
|
|
15
|
+
sessionProjectRoot,
|
|
16
|
+
sessionWorkingDir,
|
|
17
|
+
}: {
|
|
18
|
+
sessionProjectRoot: string | null;
|
|
19
|
+
sessionWorkingDir: string | null;
|
|
20
|
+
}) => {
|
|
21
|
+
mocks.panelProps({ sessionProjectRoot, sessionWorkingDir });
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
data-testid="workspace-panel"
|
|
25
|
+
data-project-root={sessionProjectRoot ?? ""}
|
|
26
|
+
data-working-dir={sessionWorkingDir ?? ""}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("@/features/cron", () => ({
|
|
33
|
+
useCronJobs: () => ({ data: { jobs: [] } }),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
class MemoryPersistStorage implements PersistStorage<unknown> {
|
|
37
|
+
private readonly values = new Map<string, StorageValue<unknown>>();
|
|
38
|
+
|
|
39
|
+
getItem = (name: string) => this.values.get(name) ?? null;
|
|
40
|
+
|
|
41
|
+
setItem = (name: string, value: StorageValue<unknown>) => {
|
|
42
|
+
this.values.set(name, value);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
removeItem = (name: string) => {
|
|
46
|
+
this.values.delete(name);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createSummary(
|
|
51
|
+
overrides: Partial<NcpSessionSummaryView> & Pick<NcpSessionSummaryView, "sessionId">,
|
|
52
|
+
): NcpSessionSummaryView {
|
|
53
|
+
return {
|
|
54
|
+
messageCount: 1,
|
|
55
|
+
status: "idle",
|
|
56
|
+
updatedAt: "2026-06-19T00:00:00.000Z",
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("ChatConversationWorkspaceSection", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
mocks.panelProps.mockReset();
|
|
64
|
+
useChatThreadStore.persist.setOptions({
|
|
65
|
+
storage: new MemoryPersistStorage(),
|
|
66
|
+
});
|
|
67
|
+
useChatThreadStore.setState({
|
|
68
|
+
snapshot: {
|
|
69
|
+
...useChatThreadStore.getState().snapshot,
|
|
70
|
+
workspacePanelParentKey: "session-1",
|
|
71
|
+
activeWorkspacePanelKind: "file",
|
|
72
|
+
workspaceFileTabs: [
|
|
73
|
+
{
|
|
74
|
+
key: "file-tab",
|
|
75
|
+
parentSessionKey: "session-1",
|
|
76
|
+
path: "docs/designs/2026-06-10-core-kernel-responsibility.design.md",
|
|
77
|
+
label: "core-kernel-responsibility",
|
|
78
|
+
viewMode: "preview",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
activeWorkspaceFileKey: "file-tab",
|
|
82
|
+
childSessionTabs: [],
|
|
83
|
+
activeChildSessionKey: null,
|
|
84
|
+
workspaceNavigationHistory: [],
|
|
85
|
+
workspaceNavigationHistoryIndex: 0,
|
|
86
|
+
sessionProjectRoot: null,
|
|
87
|
+
sessionWorkingDir: null,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
useChatQueryStore.setState({
|
|
91
|
+
snapshot: {
|
|
92
|
+
sessionsQuery: {
|
|
93
|
+
data: {
|
|
94
|
+
sessions: [
|
|
95
|
+
createSummary({
|
|
96
|
+
sessionId: "session-1",
|
|
97
|
+
workingDir: "/Users/peiwang/Projects/nextbot",
|
|
98
|
+
metadata: {
|
|
99
|
+
project_root: "/Users/peiwang/Projects/nextbot",
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
total: 1,
|
|
104
|
+
},
|
|
105
|
+
} as never,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("uses selected session workingDir as the workspace file preview base path", () => {
|
|
111
|
+
render(
|
|
112
|
+
<ChatConversationWorkspaceSection
|
|
113
|
+
layoutMode="desktop"
|
|
114
|
+
sessionKey="session-1"
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const panel = screen.getByTestId("workspace-panel");
|
|
119
|
+
expect(panel.getAttribute("data-project-root")).toBe(
|
|
120
|
+
"/Users/peiwang/Projects/nextbot",
|
|
121
|
+
);
|
|
122
|
+
expect(panel.getAttribute("data-working-dir")).toBe(
|
|
123
|
+
"/Users/peiwang/Projects/nextbot",
|
|
124
|
+
);
|
|
125
|
+
expect(mocks.panelProps).toHaveBeenCalledWith(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
sessionProjectRoot: "/Users/peiwang/Projects/nextbot",
|
|
128
|
+
sessionWorkingDir: "/Users/peiwang/Projects/nextbot",
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
import { useRef, type ReactNode } from "react";
|
|
2
2
|
import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
|
|
3
|
+
import type { NcpMessage } from "@nextclaw/ncp";
|
|
3
4
|
import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
|
|
4
|
-
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
5
5
|
|
|
6
6
|
type ChatConversationContentProps = {
|
|
7
|
+
isAwaitingAssistantOutput: boolean;
|
|
8
|
+
isHistoryLoading: boolean;
|
|
9
|
+
isSending: boolean;
|
|
10
|
+
messages: readonly NcpMessage[];
|
|
11
|
+
sessionKey: string | null;
|
|
7
12
|
showWelcome: boolean;
|
|
8
13
|
welcomeSlot?: ReactNode;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
export function ChatConversationContent({
|
|
17
|
+
isAwaitingAssistantOutput,
|
|
18
|
+
isHistoryLoading,
|
|
19
|
+
isSending,
|
|
20
|
+
messages,
|
|
21
|
+
sessionKey,
|
|
12
22
|
showWelcome,
|
|
13
23
|
welcomeSlot,
|
|
14
24
|
}: ChatConversationContentProps) {
|
|
15
|
-
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
16
25
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
17
26
|
const hideEmptyHint =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
!
|
|
21
|
-
!
|
|
27
|
+
isHistoryLoading &&
|
|
28
|
+
messages.length === 0 &&
|
|
29
|
+
!isSending &&
|
|
30
|
+
!isAwaitingAssistantOutput;
|
|
22
31
|
const { onScroll } = useStickyBottomScroll({
|
|
23
32
|
scrollRef: threadRef,
|
|
24
|
-
resetKey:
|
|
25
|
-
isLoading:
|
|
26
|
-
hasContent:
|
|
27
|
-
contentVersion:
|
|
33
|
+
resetKey: sessionKey,
|
|
34
|
+
isLoading: isHistoryLoading,
|
|
35
|
+
hasContent: messages.length > 0,
|
|
36
|
+
contentVersion: messages[messages.length - 1] ?? null,
|
|
28
37
|
});
|
|
29
|
-
const hasMessages =
|
|
30
|
-
const isAwaitingAssistantOutput =
|
|
31
|
-
hasMessages && snapshot.isSending && snapshot.isAwaitingAssistantOutput;
|
|
38
|
+
const hasMessages = messages.length > 0;
|
|
32
39
|
|
|
33
40
|
return (
|
|
34
41
|
<div
|
|
@@ -42,8 +49,8 @@ export function ChatConversationContent({
|
|
|
42
49
|
) : hideEmptyHint || !hasMessages ? null : (
|
|
43
50
|
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
|
|
44
51
|
<ChatMessageListContainer
|
|
45
|
-
messages={
|
|
46
|
-
isSending={isAwaitingAssistantOutput}
|
|
52
|
+
messages={messages}
|
|
53
|
+
isSending={hasMessages && isSending && isAwaitingAssistantOutput}
|
|
47
54
|
/>
|
|
48
55
|
</div>
|
|
49
56
|
)}
|
|
@@ -9,10 +9,10 @@ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/us
|
|
|
9
9
|
import { sessionDisplayName } from "@/features/chat/features/session/utils/chat-session-display.utils";
|
|
10
10
|
import {
|
|
11
11
|
buildSessionTypeOptions,
|
|
12
|
+
DEFAULT_SESSION_TYPE,
|
|
12
13
|
normalizeSessionType,
|
|
13
14
|
resolveSessionTypeLabel,
|
|
14
15
|
} from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
15
|
-
import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
|
|
16
16
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
17
17
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
18
18
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
@@ -34,9 +34,8 @@ export function ChatConversationHeaderSection({
|
|
|
34
34
|
const sessionKey = useChatSessionListStore(
|
|
35
35
|
(state) => state.snapshot.selectedSessionKey,
|
|
36
36
|
);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
(state) => state.snapshot.pendingSessionType,
|
|
37
|
+
const selectedAgentId = useChatSessionListStore(
|
|
38
|
+
(state) => state.snapshot.selectedAgentId,
|
|
40
39
|
);
|
|
41
40
|
const sessionTypesData = useChatQueryStore(
|
|
42
41
|
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
@@ -47,7 +46,7 @@ export function ChatConversationHeaderSection({
|
|
|
47
46
|
[sessionTypesData?.options],
|
|
48
47
|
);
|
|
49
48
|
const sessionType = normalizeSessionType(
|
|
50
|
-
selectedSession?.sessionType ??
|
|
49
|
+
selectedSession?.sessionType ?? sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE,
|
|
51
50
|
);
|
|
52
51
|
const sessionTypeOption =
|
|
53
52
|
sessionTypeOptions.find((option) => option.value === sessionType) ?? null;
|
|
@@ -58,8 +57,8 @@ export function ChatConversationHeaderSection({
|
|
|
58
57
|
selectedSession?.projectName ?? getSessionProjectName(sessionProjectRoot);
|
|
59
58
|
const canDeleteSession = Boolean(selectedSession);
|
|
60
59
|
const { childSessionTabs, sessionCronJobs } =
|
|
61
|
-
useChatConversationWorkspaceState(snapshot);
|
|
62
|
-
const shouldShowSessionHeader = Boolean(
|
|
60
|
+
useChatConversationWorkspaceState(snapshot, sessionKey);
|
|
61
|
+
const shouldShowSessionHeader = Boolean(sessionKey || sessionTypeLabel);
|
|
63
62
|
const sessionHeaderTitle =
|
|
64
63
|
(selectedSession ? sessionDisplayName(selectedSession) : undefined) ||
|
|
65
64
|
(canDeleteSession && sessionKey
|
|
@@ -67,23 +66,23 @@ export function ChatConversationHeaderSection({
|
|
|
67
66
|
: null) ||
|
|
68
67
|
t("chatSidebarNewTask");
|
|
69
68
|
const normalizedAgentId =
|
|
70
|
-
selectedSession?.agentId?.trim() ??
|
|
69
|
+
selectedSession?.agentId?.trim() ?? selectedAgentId?.trim() ?? "";
|
|
71
70
|
const shouldShowHeaderAgentAvatar =
|
|
72
71
|
normalizedAgentId.length > 0 && normalizedAgentId.toLowerCase() !== "main";
|
|
73
72
|
const openChildSessions = () => {
|
|
74
|
-
if (!
|
|
73
|
+
if (!sessionKey) {
|
|
75
74
|
return;
|
|
76
75
|
}
|
|
77
76
|
presenter.chatThreadManager.openChildSessionPanel({
|
|
78
|
-
parentSessionKey:
|
|
77
|
+
parentSessionKey: sessionKey,
|
|
79
78
|
activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
|
|
80
79
|
});
|
|
81
80
|
};
|
|
82
81
|
const openSessionCronJobs = () => {
|
|
83
|
-
if (!
|
|
82
|
+
if (!sessionKey || sessionCronJobs.length === 0) {
|
|
84
83
|
return;
|
|
85
84
|
}
|
|
86
|
-
presenter.chatThreadManager.openSessionCronPanel(
|
|
85
|
+
presenter.chatThreadManager.openSessionCronPanel(sessionKey);
|
|
87
86
|
};
|
|
88
87
|
|
|
89
88
|
return (
|
|
@@ -104,7 +103,7 @@ export function ChatConversationHeaderSection({
|
|
|
104
103
|
}
|
|
105
104
|
sessionTypeBadge={
|
|
106
105
|
sessionTypeLabel ? (
|
|
107
|
-
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-
|
|
106
|
+
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
108
107
|
{sessionTypeOption?.icon?.src ? (
|
|
109
108
|
<span className="inline-flex h-[1.125rem] w-[1.125rem] items-center justify-center">
|
|
110
109
|
<SessionContextIconNode
|
|
@@ -124,7 +123,7 @@ export function ChatConversationHeaderSection({
|
|
|
124
123
|
projectBadge={
|
|
125
124
|
sessionProjectName ? (
|
|
126
125
|
<ChatSessionProjectBadge
|
|
127
|
-
sessionKey={
|
|
126
|
+
sessionKey={sessionKey ?? "draft"}
|
|
128
127
|
projectName={sessionProjectName}
|
|
129
128
|
projectRoot={sessionProjectRoot}
|
|
130
129
|
persistToServer={canDeleteSession}
|
|
@@ -132,9 +131,9 @@ export function ChatConversationHeaderSection({
|
|
|
132
131
|
) : null
|
|
133
132
|
}
|
|
134
133
|
actions={
|
|
135
|
-
|
|
134
|
+
sessionKey ? (
|
|
136
135
|
<ChatSessionHeaderActions
|
|
137
|
-
sessionKey={
|
|
136
|
+
sessionKey={sessionKey}
|
|
138
137
|
canDeleteSession={canDeleteSession}
|
|
139
138
|
isDeletePending={snapshot.isDeletePending}
|
|
140
139
|
projectRoot={sessionProjectRoot}
|
|
@@ -16,11 +16,11 @@ export function ChatParentSessionBanner({
|
|
|
16
16
|
}
|
|
17
17
|
const trimmedLabel = parentSessionLabel.trim();
|
|
18
18
|
return (
|
|
19
|
-
<div className="
|
|
19
|
+
<div className="bg-background/75 px-4 py-2 backdrop-blur-sm sm:px-5">
|
|
20
20
|
<button
|
|
21
21
|
type="button"
|
|
22
22
|
onClick={onGoToParentSession}
|
|
23
|
-
className="inline-flex items-center gap-2 text-xs font-medium text-
|
|
23
|
+
className="inline-flex items-center gap-2 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
24
24
|
>
|
|
25
25
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
26
26
|
<span>
|
|
@@ -55,10 +55,11 @@ export function ChatConversationHeader({
|
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<div
|
|
58
|
+
data-testid="chat-conversation-header"
|
|
58
59
|
className={cn(
|
|
59
|
-
"
|
|
60
|
+
"bg-background/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-colors duration-200",
|
|
60
61
|
isMobileLayout ? "px-3 sm:px-3" : "px-4 sm:px-5",
|
|
61
|
-
shouldShow ? "opacity-100" : "h-0 py-0 opacity-0
|
|
62
|
+
shouldShow ? "opacity-100" : "h-0 py-0 opacity-0",
|
|
62
63
|
shouldShow && (isMobileLayout ? "min-h-12 pb-2 pt-2" : "h-[52px]"),
|
|
63
64
|
)}
|
|
64
65
|
style={
|
|
@@ -73,11 +74,11 @@ export function ChatConversationHeader({
|
|
|
73
74
|
icon={<ArrowLeft className="h-4 w-4" />}
|
|
74
75
|
label={t("chat")}
|
|
75
76
|
onClick={onBackToList}
|
|
76
|
-
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-
|
|
77
|
+
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
77
78
|
/>
|
|
78
79
|
) : null}
|
|
79
80
|
{leading}
|
|
80
|
-
<span className="text-sm font-medium text-
|
|
81
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
81
82
|
{title}
|
|
82
83
|
</span>
|
|
83
84
|
{sessionTypeBadge}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ChatConversationAlerts } from "@/features/chat/components/conversation/chat-conversation-alerts";
|
|
3
|
-
import { ChatConversationContent } from "@/features/chat/components/conversation/chat-conversation-content";
|
|
1
|
+
import { useCallback } from "react";
|
|
4
2
|
import { ChatConversationHeaderSection } from "@/features/chat/components/conversation/chat-conversation-header-section";
|
|
5
3
|
import { ChatConversationParentBanner } from "@/features/chat/components/conversation/chat-conversation-parent-banner";
|
|
6
4
|
import { ChatConversationSkeleton } from "@/features/chat/components/conversation/chat-conversation-skeleton";
|
|
7
5
|
import { ChatConversationWorkspaceSection } from "@/features/chat/components/conversation/chat-conversation-workspace-section";
|
|
6
|
+
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
7
|
+
import { SessionConversationArea } from "@/features/chat/features/conversation/components/session-conversation-area";
|
|
8
8
|
import { useNcpChatProviderStateResolved } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
|
|
9
|
-
import {
|
|
10
|
-
import { shouldShowChatWelcome } from "@/features/chat/features/welcome/utils/chat-welcome-visibility.utils";
|
|
11
|
-
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
9
|
+
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
12
10
|
|
|
13
11
|
type ChatConversationLayoutMode = "desktop" | "mobile";
|
|
14
12
|
|
|
@@ -19,36 +17,38 @@ export function ChatConversationPanel({
|
|
|
19
17
|
layoutMode?: ChatConversationLayoutMode;
|
|
20
18
|
onBackToList?: () => void;
|
|
21
19
|
}) {
|
|
20
|
+
const presenter = usePresenter();
|
|
22
21
|
const isProviderStateResolved = useNcpChatProviderStateResolved();
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
const sessionKey = useChatSessionListStore(
|
|
23
|
+
(state) => state.snapshot.selectedSessionKey,
|
|
25
24
|
);
|
|
25
|
+
const handleSessionMaterialized = useCallback((materializedSessionKey: string) => {
|
|
26
|
+
if (!presenter.chatUiManager.isAtChatRoot()) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
presenter.chatUiManager.goToSession(materializedSessionKey, { replace: true });
|
|
30
|
+
}, [presenter]);
|
|
26
31
|
|
|
27
32
|
if (!isProviderStateResolved) {
|
|
28
33
|
return <ChatConversationSkeleton />;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
return (
|
|
32
|
-
<section className="flex-1 min-h-0 flex overflow-hidden bg-
|
|
37
|
+
<section className="flex-1 min-h-0 flex overflow-hidden bg-background">
|
|
33
38
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
34
39
|
<ChatConversationParentBanner />
|
|
35
40
|
<ChatConversationHeaderSection
|
|
36
41
|
layoutMode={layoutMode}
|
|
37
42
|
onBackToList={onBackToList}
|
|
38
43
|
/>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<ChatConversationWelcome
|
|
44
|
-
inputSlot={<ChatInputBarContainer surface="embedded" />}
|
|
45
|
-
/>
|
|
46
|
-
}
|
|
44
|
+
<SessionConversationArea
|
|
45
|
+
consumeDraftIntent
|
|
46
|
+
sessionKey={sessionKey}
|
|
47
|
+
onSessionMaterialized={handleSessionMaterialized}
|
|
47
48
|
/>
|
|
48
|
-
{showWelcome ? null : <ChatInputBarContainer />}
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
|
-
<ChatConversationWorkspaceSection layoutMode={layoutMode} />
|
|
51
|
+
<ChatConversationWorkspaceSection layoutMode={layoutMode} sessionKey={sessionKey} />
|
|
52
52
|
</section>
|
|
53
53
|
);
|
|
54
54
|
}
|
|
@@ -28,13 +28,13 @@ export function ChatConversationSkeleton() {
|
|
|
28
28
|
return (
|
|
29
29
|
<section
|
|
30
30
|
data-testid="chat-conversation-skeleton"
|
|
31
|
-
className="flex-1 min-h-0 flex flex-col overflow-hidden bg-
|
|
31
|
+
className="flex-1 min-h-0 flex flex-col overflow-hidden bg-background"
|
|
32
32
|
>
|
|
33
33
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
|
|
34
34
|
<div className="mx-auto flex min-h-full w-full max-w-[min(1120px,100%)] flex-col px-6 py-5">
|
|
35
35
|
<div className="flex flex-1 flex-col gap-8">
|
|
36
36
|
<div className="space-y-6">
|
|
37
|
-
<Skeleton className="h-6 w-52 rounded-lg bg-
|
|
37
|
+
<Skeleton className="h-6 w-52 rounded-lg bg-muted" />
|
|
38
38
|
<div className="space-y-5">
|
|
39
39
|
{CHAT_CONVERSATION_SKELETON_BUBBLES.map((bubble) => (
|
|
40
40
|
<div
|
|
@@ -44,7 +44,7 @@ export function ChatConversationSkeleton() {
|
|
|
44
44
|
<Skeleton
|
|
45
45
|
data-testid="chat-conversation-skeleton-bubble"
|
|
46
46
|
className={cn(
|
|
47
|
-
"w-full bg-
|
|
47
|
+
"w-full bg-muted",
|
|
48
48
|
bubble.bubbleClassName,
|
|
49
49
|
)}
|
|
50
50
|
/>
|
|
@@ -54,29 +54,29 @@ export function ChatConversationSkeleton() {
|
|
|
54
54
|
</div>
|
|
55
55
|
<div className="mt-auto grid gap-4 pb-2 sm:grid-cols-[minmax(0,1fr)_minmax(180px,240px)] sm:items-end">
|
|
56
56
|
<div className="space-y-3">
|
|
57
|
-
<Skeleton className="h-4 w-40 rounded-full bg-
|
|
58
|
-
<Skeleton className="h-[112px] w-full rounded-[30px] bg-
|
|
57
|
+
<Skeleton className="h-4 w-40 rounded-full bg-muted" />
|
|
58
|
+
<Skeleton className="h-[112px] w-full rounded-[30px] bg-muted" />
|
|
59
59
|
</div>
|
|
60
60
|
<div className="hidden justify-end sm:flex">
|
|
61
|
-
<Skeleton className="h-10 w-36 rounded-full bg-
|
|
61
|
+
<Skeleton className="h-10 w-36 rounded-full bg-muted" />
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
67
|
-
<div className="
|
|
67
|
+
<div className="bg-background p-4">
|
|
68
68
|
<div className="mx-auto w-full max-w-[min(1120px,100%)]">
|
|
69
|
-
<div className="overflow-hidden rounded-2xl border border-
|
|
69
|
+
<div className="overflow-hidden rounded-2xl border border-border bg-card shadow-card">
|
|
70
70
|
<div className="px-4 py-2.5">
|
|
71
|
-
<Skeleton className="h-[84px] w-full rounded-[28px] bg-
|
|
71
|
+
<Skeleton className="h-[84px] w-full rounded-[28px] bg-muted" />
|
|
72
72
|
</div>
|
|
73
73
|
<div className="flex items-center justify-between gap-3 px-3 pb-3">
|
|
74
74
|
<div className="flex items-center gap-2">
|
|
75
|
-
<Skeleton className="h-8 w-20 rounded-full bg-
|
|
76
|
-
<Skeleton className="h-8 w-28 rounded-full bg-
|
|
77
|
-
<Skeleton className="hidden h-8 w-24 rounded-full bg-
|
|
75
|
+
<Skeleton className="h-8 w-20 rounded-full bg-muted" />
|
|
76
|
+
<Skeleton className="h-8 w-28 rounded-full bg-muted" />
|
|
77
|
+
<Skeleton className="hidden h-8 w-24 rounded-full bg-muted sm:block" />
|
|
78
78
|
</div>
|
|
79
|
-
<Skeleton className="h-8 w-8 rounded-full bg-
|
|
79
|
+
<Skeleton className="h-8 w-8 rounded-full bg-muted" />
|
|
80
80
|
</div>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { ChatSessionWorkspacePanel } from "@/features/chat/features/workspace/components/chat-session-workspace-panel";
|
|
2
|
+
import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
|
|
2
3
|
import { useChatConversationWorkspaceState } from "@/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state";
|
|
3
4
|
import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
|
|
4
5
|
|
|
5
6
|
type ChatConversationWorkspaceSectionProps = {
|
|
6
7
|
layoutMode: "desktop" | "mobile";
|
|
8
|
+
sessionKey: string | null;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
export function ChatConversationWorkspaceSection({
|
|
10
12
|
layoutMode,
|
|
13
|
+
sessionKey,
|
|
11
14
|
}: ChatConversationWorkspaceSectionProps) {
|
|
12
15
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
16
|
+
const selectedSession = useNcpChatSelectedSession(sessionKey);
|
|
13
17
|
const {
|
|
14
18
|
childSessionTabs,
|
|
19
|
+
activeSideChatDraft,
|
|
15
20
|
workspaceFileTabs,
|
|
16
21
|
sessionCronJobs,
|
|
17
22
|
showWorkspacePanel,
|
|
18
|
-
} = useChatConversationWorkspaceState(snapshot);
|
|
23
|
+
} = useChatConversationWorkspaceState(snapshot, sessionKey);
|
|
19
24
|
|
|
20
25
|
if (!showWorkspacePanel) {
|
|
21
26
|
return null;
|
|
@@ -23,18 +28,19 @@ export function ChatConversationWorkspaceSection({
|
|
|
23
28
|
|
|
24
29
|
return (
|
|
25
30
|
<ChatSessionWorkspacePanel
|
|
26
|
-
sessionKey={
|
|
31
|
+
sessionKey={sessionKey}
|
|
27
32
|
childSessionTabs={childSessionTabs}
|
|
28
33
|
activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
|
|
34
|
+
activeSideChatDraft={activeSideChatDraft}
|
|
29
35
|
workspaceFileTabs={workspaceFileTabs}
|
|
30
36
|
activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
|
|
31
37
|
workspaceNavigationHistory={snapshot.workspaceNavigationHistory}
|
|
32
38
|
workspaceNavigationHistoryIndex={snapshot.workspaceNavigationHistoryIndex}
|
|
33
39
|
activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
|
|
34
40
|
sessionCronJobs={sessionCronJobs}
|
|
35
|
-
sessionProjectRoot={
|
|
41
|
+
sessionProjectRoot={selectedSession?.projectRoot ?? null}
|
|
36
42
|
sessionWorkingDir={
|
|
37
|
-
|
|
43
|
+
selectedSession?.workingDir ?? selectedSession?.projectRoot ?? null
|
|
38
44
|
}
|
|
39
45
|
displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
|
|
40
46
|
/>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { render, waitFor } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
3
|
import { MemoryRouter } from "react-router-dom";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { ChatSidebar } from "@/features/chat/components/layout/chat-sidebar";
|
|
5
6
|
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
6
7
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
7
8
|
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
9
|
+
import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
|
|
8
10
|
|
|
9
11
|
const mocks = vi.hoisted(() => ({
|
|
10
12
|
sessionItems: [] as NcpSessionListItemView[],
|
|
@@ -107,6 +109,8 @@ function createRunningSessionItem(
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
function resetReadStateTestState() {
|
|
112
|
+
window.localStorage.clear();
|
|
113
|
+
viewportLayoutManager.resetForTests();
|
|
110
114
|
mocks.sessionItems = [];
|
|
111
115
|
useChatQueryStore.setState({
|
|
112
116
|
snapshot: {
|
|
@@ -130,16 +134,36 @@ function resetReadStateTestState() {
|
|
|
130
134
|
});
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
function createTestQueryClient() {
|
|
138
|
+
return new QueryClient({
|
|
139
|
+
defaultOptions: {
|
|
140
|
+
queries: { retry: false },
|
|
141
|
+
mutations: { retry: false },
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createReadStateSidebarElement(queryClient: QueryClient) {
|
|
147
|
+
return (
|
|
148
|
+
<QueryClientProvider client={queryClient}>
|
|
149
|
+
<MemoryRouter>
|
|
150
|
+
<ChatSidebar />
|
|
151
|
+
</MemoryRouter>
|
|
152
|
+
</QueryClientProvider>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderReadStateSidebar(queryClient: QueryClient) {
|
|
157
|
+
return render(createReadStateSidebarElement(queryClient));
|
|
158
|
+
}
|
|
159
|
+
|
|
133
160
|
describe("ChatSidebar read state sync", () => {
|
|
134
161
|
beforeEach(resetReadStateTestState);
|
|
135
162
|
|
|
136
163
|
it("waits until the active running session is idle before persisting the read watermark", async () => {
|
|
137
164
|
mocks.sessionItems = [createRunningSessionItem("running")];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
<ChatSidebar />
|
|
141
|
-
</MemoryRouter>,
|
|
142
|
-
);
|
|
165
|
+
const queryClient = createTestQueryClient();
|
|
166
|
+
const { rerender } = renderReadStateSidebar(queryClient);
|
|
143
167
|
|
|
144
168
|
await waitFor(() => {
|
|
145
169
|
expect(
|
|
@@ -150,11 +174,7 @@ describe("ChatSidebar read state sync", () => {
|
|
|
150
174
|
});
|
|
151
175
|
|
|
152
176
|
mocks.sessionItems = [createRunningSessionItem()];
|
|
153
|
-
rerender(
|
|
154
|
-
<MemoryRouter>
|
|
155
|
-
<ChatSidebar />
|
|
156
|
-
</MemoryRouter>,
|
|
157
|
-
);
|
|
177
|
+
rerender(createReadStateSidebarElement(queryClient));
|
|
158
178
|
|
|
159
179
|
await waitFor(() => {
|
|
160
180
|
expect(
|
|
@@ -164,4 +184,25 @@ describe("ChatSidebar read state sync", () => {
|
|
|
164
184
|
).toBe("2026-03-19T09:05:00.000Z");
|
|
165
185
|
});
|
|
166
186
|
});
|
|
187
|
+
|
|
188
|
+
it("collapses the desktop sidebar into accessible icon actions", () => {
|
|
189
|
+
const { container } = renderReadStateSidebar(createTestQueryClient());
|
|
190
|
+
|
|
191
|
+
fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
|
|
192
|
+
|
|
193
|
+
const persistedState = JSON.parse(
|
|
194
|
+
window.localStorage.getItem("nextclaw.app.viewport-layout") ?? "{}",
|
|
195
|
+
) as { state?: { isSidebarCollapsed?: boolean } };
|
|
196
|
+
|
|
197
|
+
expect(
|
|
198
|
+
container.querySelector("aside")?.getAttribute("data-sidebar-collapsed"),
|
|
199
|
+
).toBe("true");
|
|
200
|
+
expect(persistedState.state?.isSidebarCollapsed).toBe(true);
|
|
201
|
+
expect(screen.getByRole("button", { name: "New Task" }).textContent).toBe(
|
|
202
|
+
"",
|
|
203
|
+
);
|
|
204
|
+
expect(screen.queryByPlaceholderText("Search conversations...")).toBeNull();
|
|
205
|
+
expect(screen.getByRole("link", { name: "Scheduled Tasks" })).toBeTruthy();
|
|
206
|
+
expect(screen.getByRole("button", { name: "Settings menu" })).toBeTruthy();
|
|
207
|
+
});
|
|
167
208
|
});
|