@nextclaw/ui 0.15.5 → 0.15.7
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-V8m62-13.js +13 -0
- package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
- package/dist/assets/index-DtWf7kk9.css +1 -0
- package/dist/assets/index-UnY4HtVa.js +2 -0
- package/dist/assets/loader-circle-BRSfaztA.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
- package/dist/assets/remote-C1y3ARHu.js +1 -0
- package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
- package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/use-config-CY4uF2Ou.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
- package/dist/assets/x-C6cM8wkT.js +1 -0
- package/dist/index.html +30 -29
- package/index.html +1 -0
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
- package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
- package/src/features/marketplace/components/marketplace-page.tsx +10 -2
- package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
- package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/index-CT0iYlxV.js +0 -2
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Check,
|
|
4
3
|
ChevronDown,
|
|
5
4
|
Loader2,
|
|
6
5
|
MessageSquareText,
|
|
7
6
|
Search,
|
|
8
7
|
} from "lucide-react";
|
|
9
8
|
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
9
|
+
import { useNcpSessionListView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
sessionDisplayName,
|
|
20
|
-
} from "@/features/chat/features/session/utils/chat-session-display.utils";
|
|
21
|
-
import { sortSessionItemsByActivityAtDesc } from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
|
|
22
|
-
import { resolveSessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
|
|
23
|
-
import {
|
|
24
|
-
buildSessionTypeOptions,
|
|
25
|
-
type ChatSessionTypeOption,
|
|
26
|
-
} from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
27
|
-
import {
|
|
28
|
-
shouldShowUnreadSessionIndicator,
|
|
29
|
-
useChatSessionListStore,
|
|
30
|
-
} from "@/features/chat/stores/chat-session-list.store";
|
|
11
|
+
groupSessionsByDate,
|
|
12
|
+
groupSessionsByProject,
|
|
13
|
+
sortSessionItemsByActivityAtDesc,
|
|
14
|
+
} from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
|
|
15
|
+
import { ChatSidebarListModeSwitch } from "@/features/chat/components/chat-sidebar-list-mode-switch";
|
|
16
|
+
import { buildSessionTypeOptions } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
17
|
+
import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
|
|
18
|
+
import { ChatSessionSwitcherGroups } from "@/features/chat/features/session/components/session-header/chat-session-switcher-groups";
|
|
31
19
|
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
32
20
|
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
33
21
|
import {
|
|
@@ -38,6 +26,7 @@ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-cont
|
|
|
38
26
|
import { Input } from "@/shared/components/ui/input";
|
|
39
27
|
import { t } from "@/shared/lib/i18n";
|
|
40
28
|
import { cn } from "@/shared/lib/utils";
|
|
29
|
+
import { useProjects } from "@/shared/hooks/use-projects";
|
|
41
30
|
|
|
42
31
|
const SWITCHER_TRIGGER_CLASS =
|
|
43
32
|
"group inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border";
|
|
@@ -68,96 +57,6 @@ function ChatSessionTitleSwitcherLoadingState() {
|
|
|
68
57
|
);
|
|
69
58
|
}
|
|
70
59
|
|
|
71
|
-
function ChatSessionSwitchItem({
|
|
72
|
-
item,
|
|
73
|
-
selectedSessionKey,
|
|
74
|
-
optimisticReadAtBySessionKey,
|
|
75
|
-
sessionTypeOptions,
|
|
76
|
-
onSelect,
|
|
77
|
-
}: {
|
|
78
|
-
item: NcpSessionListItemView;
|
|
79
|
-
selectedSessionKey: string | null;
|
|
80
|
-
optimisticReadAtBySessionKey: Record<string, string>;
|
|
81
|
-
sessionTypeOptions: ChatSessionTypeOption[];
|
|
82
|
-
onSelect: (sessionKey: string) => void;
|
|
83
|
-
}) {
|
|
84
|
-
const { session, runStatus } = item;
|
|
85
|
-
const active = selectedSessionKey === session.key;
|
|
86
|
-
const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
|
|
87
|
-
const effectiveReadAt =
|
|
88
|
-
optimisticReadAt && session.readAt
|
|
89
|
-
? optimisticReadAt.localeCompare(session.readAt) > 0
|
|
90
|
-
? optimisticReadAt
|
|
91
|
-
: session.readAt
|
|
92
|
-
: (optimisticReadAt ?? session.readAt);
|
|
93
|
-
const shouldShowUnread = shouldShowUnreadSessionIndicator({
|
|
94
|
-
active,
|
|
95
|
-
lastMessageAt: session.lastMessageAt,
|
|
96
|
-
readAt: effectiveReadAt,
|
|
97
|
-
runStatus,
|
|
98
|
-
});
|
|
99
|
-
const previewText =
|
|
100
|
-
sessionActivityPreviewText(session) ??
|
|
101
|
-
session.projectName ??
|
|
102
|
-
session.projectRoot ??
|
|
103
|
-
`${session.messageCount}`;
|
|
104
|
-
const trailingText = formatSessionListTime(
|
|
105
|
-
session.lastMessageAt ?? session.createdAt,
|
|
106
|
-
);
|
|
107
|
-
const sessionContext = resolveSessionContextView(session, sessionTypeOptions);
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<button
|
|
111
|
-
type="button"
|
|
112
|
-
aria-current={active ? "page" : undefined}
|
|
113
|
-
className={cn(
|
|
114
|
-
"flex w-full min-w-0 items-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
|
|
115
|
-
active
|
|
116
|
-
? "bg-accent text-accent-foreground"
|
|
117
|
-
: "text-popover-foreground hover:bg-accent/70",
|
|
118
|
-
)}
|
|
119
|
-
onClick={() => onSelect(session.key)}
|
|
120
|
-
>
|
|
121
|
-
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center">
|
|
122
|
-
{active ? (
|
|
123
|
-
<Check className="h-3.5 w-3.5" />
|
|
124
|
-
) : shouldShowUnread ? (
|
|
125
|
-
<span
|
|
126
|
-
aria-label={t("chatSessionUnread")}
|
|
127
|
-
className="h-2 w-2 rounded-full bg-primary"
|
|
128
|
-
/>
|
|
129
|
-
) : null}
|
|
130
|
-
</span>
|
|
131
|
-
<span className="min-w-0 flex-1">
|
|
132
|
-
<span className="flex min-w-0 items-center gap-1.5">
|
|
133
|
-
<span className="min-w-0 truncate text-[13px] font-medium">
|
|
134
|
-
{sessionDisplayName(session)}
|
|
135
|
-
</span>
|
|
136
|
-
{sessionContext.label ? (
|
|
137
|
-
<span className="shrink-0 rounded-full border border-border/70 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground">
|
|
138
|
-
{sessionContext.label}
|
|
139
|
-
</span>
|
|
140
|
-
) : null}
|
|
141
|
-
{sessionContext.icon ? (
|
|
142
|
-
<span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center text-muted-foreground">
|
|
143
|
-
<SessionContextIconNode icon={sessionContext.icon} />
|
|
144
|
-
</span>
|
|
145
|
-
) : null}
|
|
146
|
-
</span>
|
|
147
|
-
<span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
|
|
148
|
-
<span className="min-w-0 truncate">{previewText}</span>
|
|
149
|
-
<span className="shrink-0">{trailingText}</span>
|
|
150
|
-
</span>
|
|
151
|
-
</span>
|
|
152
|
-
{runStatus ? (
|
|
153
|
-
<span className="mt-0.5 shrink-0">
|
|
154
|
-
<SessionRunBadge status={runStatus} />
|
|
155
|
-
</span>
|
|
156
|
-
) : null}
|
|
157
|
-
</button>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
60
|
function ChatSessionTitleSwitcherPopover({
|
|
162
61
|
selectedSessionKey,
|
|
163
62
|
title,
|
|
@@ -171,14 +70,51 @@ function ChatSessionTitleSwitcherPopover({
|
|
|
171
70
|
const optimisticReadAtBySessionKey = useChatSessionListStore(
|
|
172
71
|
(state) => state.optimisticReadAtBySessionKey,
|
|
173
72
|
);
|
|
73
|
+
const pinnedSessionKeys = useChatSessionListStore(
|
|
74
|
+
(state) => state.snapshot.pinnedSessionKeys,
|
|
75
|
+
);
|
|
76
|
+
const pinnedProjectRoots = useChatSessionListStore(
|
|
77
|
+
(state) => state.snapshot.pinnedProjectRoots,
|
|
78
|
+
);
|
|
79
|
+
const collapsedProjectRoots = useChatSessionListStore(
|
|
80
|
+
(state) => state.snapshot.collapsedProjectRoots,
|
|
81
|
+
);
|
|
82
|
+
const listMode = useChatSessionListStore(
|
|
83
|
+
(state) => state.snapshot.listMode,
|
|
84
|
+
);
|
|
174
85
|
const sessionTypesData = useChatQueryStore(
|
|
175
86
|
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
176
87
|
);
|
|
177
88
|
const { isLoading, items } = useNcpSessionListView({ query: searchQuery });
|
|
89
|
+
const projectsQuery = useProjects();
|
|
90
|
+
const visibleProjects = useMemo(() => {
|
|
91
|
+
const query = searchQuery.trim().toLowerCase();
|
|
92
|
+
const projects = projectsQuery.data?.projects ?? [];
|
|
93
|
+
if (!query) {
|
|
94
|
+
return projects;
|
|
95
|
+
}
|
|
96
|
+
return projects.filter((project) =>
|
|
97
|
+
project.name.toLowerCase().includes(query) ||
|
|
98
|
+
project.rootPath.toLowerCase().includes(query)
|
|
99
|
+
);
|
|
100
|
+
}, [projectsQuery.data?.projects, searchQuery]);
|
|
178
101
|
const sortedItems = useMemo(
|
|
179
102
|
() => sortSessionItemsByActivityAtDesc(items),
|
|
180
103
|
[items],
|
|
181
104
|
);
|
|
105
|
+
const dateGroups = useMemo(
|
|
106
|
+
() => groupSessionsByDate(sortedItems, new Set(pinnedSessionKeys)),
|
|
107
|
+
[pinnedSessionKeys, sortedItems],
|
|
108
|
+
);
|
|
109
|
+
const projectGroups = useMemo(
|
|
110
|
+
() => groupSessionsByProject(
|
|
111
|
+
sortedItems,
|
|
112
|
+
new Set(pinnedSessionKeys),
|
|
113
|
+
new Set(pinnedProjectRoots),
|
|
114
|
+
visibleProjects,
|
|
115
|
+
),
|
|
116
|
+
[pinnedProjectRoots, pinnedSessionKeys, sortedItems, visibleProjects],
|
|
117
|
+
);
|
|
182
118
|
const sessionTypeOptions = useMemo(
|
|
183
119
|
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
184
120
|
[sessionTypesData?.options],
|
|
@@ -198,6 +134,16 @@ function ChatSessionTitleSwitcherPopover({
|
|
|
198
134
|
setSearchQuery("");
|
|
199
135
|
}
|
|
200
136
|
};
|
|
137
|
+
const sessionListProps = {
|
|
138
|
+
selectedSessionKey,
|
|
139
|
+
optimisticReadAtBySessionKey,
|
|
140
|
+
sessionTypeOptions,
|
|
141
|
+
onSelect: selectSession,
|
|
142
|
+
};
|
|
143
|
+
const isProjectFirstView = listMode === "project-first";
|
|
144
|
+
const hasVisibleGroups = isProjectFirstView
|
|
145
|
+
? projectGroups.length > 0
|
|
146
|
+
: dateGroups.length > 0;
|
|
201
147
|
|
|
202
148
|
return (
|
|
203
149
|
<Popover open={isOpen} onOpenChange={updateOpen}>
|
|
@@ -222,8 +168,14 @@ function ChatSessionTitleSwitcherPopover({
|
|
|
222
168
|
className="w-[22rem] max-w-[calc(100vw-2rem)] p-0"
|
|
223
169
|
>
|
|
224
170
|
<div className="space-y-2 border-b border-border px-3 py-2">
|
|
225
|
-
<div className="
|
|
226
|
-
|
|
171
|
+
<div className="flex items-center justify-between gap-3">
|
|
172
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
|
|
173
|
+
{t("chatSessionSwitcherTitle")}
|
|
174
|
+
</div>
|
|
175
|
+
<ChatSidebarListModeSwitch
|
|
176
|
+
isProjectFirstView={isProjectFirstView}
|
|
177
|
+
onSelectMode={presenter.chatSessionListManager.setListMode}
|
|
178
|
+
/>
|
|
227
179
|
</div>
|
|
228
180
|
<div className="relative">
|
|
229
181
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/65" />
|
|
@@ -240,25 +192,27 @@ function ChatSessionTitleSwitcherPopover({
|
|
|
240
192
|
<div className="max-h-80 overflow-y-auto p-1.5">
|
|
241
193
|
{isLoading ? (
|
|
242
194
|
<ChatSessionTitleSwitcherLoadingState />
|
|
243
|
-
) :
|
|
244
|
-
<
|
|
245
|
-
{
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
))}
|
|
255
|
-
</div>
|
|
195
|
+
) : hasVisibleGroups ? (
|
|
196
|
+
<ChatSessionSwitcherGroups
|
|
197
|
+
collapsedProjectRoots={collapsedProjectRoots}
|
|
198
|
+
dateGroups={dateGroups}
|
|
199
|
+
isProjectFirstView={isProjectFirstView}
|
|
200
|
+
onToggleProjectCollapsed={
|
|
201
|
+
presenter.chatSessionListManager.toggleProjectCollapsed
|
|
202
|
+
}
|
|
203
|
+
projectGroups={projectGroups}
|
|
204
|
+
{...sessionListProps}
|
|
205
|
+
/>
|
|
256
206
|
) : (
|
|
257
207
|
<ChatSessionTitleSwitcherEmptyState
|
|
258
208
|
label={
|
|
259
209
|
searchQuery.trim()
|
|
260
210
|
? t("chatSessionSwitcherNoResults")
|
|
261
|
-
: t(
|
|
211
|
+
: t(
|
|
212
|
+
isProjectFirstView
|
|
213
|
+
? "chatSidebarProjectViewEmpty"
|
|
214
|
+
: "sessionsEmpty",
|
|
215
|
+
)
|
|
262
216
|
}
|
|
263
217
|
/>
|
|
264
218
|
)}
|
|
@@ -281,7 +235,7 @@ export function ChatSessionTitleSwitcher({
|
|
|
281
235
|
(state) => state.isSidebarCollapsed,
|
|
282
236
|
);
|
|
283
237
|
|
|
284
|
-
if (layoutMode
|
|
238
|
+
if (layoutMode === "desktop" && !isSidebarCollapsed) {
|
|
285
239
|
return <ChatSessionTitle title={title} />;
|
|
286
240
|
}
|
|
287
241
|
|
package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { groupSessionsByProject } from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
|
|
3
|
+
|
|
4
|
+
describe("groupSessionsByProject", () => {
|
|
5
|
+
it("uses registered projects as the project-first baseline", () => {
|
|
6
|
+
const groups = groupSessionsByProject([], new Set(), new Set(), [{
|
|
7
|
+
name: "Knowledge",
|
|
8
|
+
rootPath: "/tmp/knowledge",
|
|
9
|
+
template: "knowledge-base",
|
|
10
|
+
createdAt: "2026-07-15T00:00:00.000Z",
|
|
11
|
+
updatedAt: "2026-07-15T00:00:00.000Z",
|
|
12
|
+
}]);
|
|
13
|
+
|
|
14
|
+
expect(groups).toEqual([{
|
|
15
|
+
projectRoot: "/tmp/knowledge",
|
|
16
|
+
projectName: "Knowledge",
|
|
17
|
+
items: [],
|
|
18
|
+
latestUpdatedAt: new Date("2026-07-15T00:00:00.000Z").getTime(),
|
|
19
|
+
isPinned: false,
|
|
20
|
+
}]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
CHAT_SESSION_MATERIALIZATION_METADATA_KEY,
|
|
3
3
|
type AgentRunSessionMaterializationMetadata,
|
|
4
4
|
} from "@nextclaw/shared";
|
|
5
|
-
import {
|
|
5
|
+
import { buildInlineTokensFromComposer, CHAT_INLINE_TOKENS_METADATA_KEY } from "@/features/chat/features/input/utils/chat-inline-token.utils";
|
|
6
6
|
import { normalizeRequestedSkills } from "@/features/chat/features/runtime/utils/chat-runtime.utils";
|
|
7
7
|
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ export function buildChatRunMetadata(payload: {
|
|
|
20
20
|
sessionType?: string;
|
|
21
21
|
projectRoot?: string | null;
|
|
22
22
|
requestedSkills?: string[];
|
|
23
|
-
composerNodes?: Parameters<typeof
|
|
23
|
+
composerNodes?: Parameters<typeof buildInlineTokensFromComposer>[0];
|
|
24
24
|
sessionMaterialization?: AgentRunSessionMaterializationMetadata | null;
|
|
25
25
|
}): Record<string, unknown> {
|
|
26
26
|
const projectRoot = normalizeSessionProjectRootValue(payload.projectRoot);
|
|
@@ -35,11 +35,11 @@ export function buildChatRunMetadata(payload: {
|
|
|
35
35
|
if (requestedSkills.length > 0) {
|
|
36
36
|
metadata.requested_skill_refs = requestedSkills;
|
|
37
37
|
}
|
|
38
|
-
const
|
|
39
|
-
?
|
|
38
|
+
const inlineTokens = payload.composerNodes
|
|
39
|
+
? buildInlineTokensFromComposer(payload.composerNodes)
|
|
40
40
|
: [];
|
|
41
|
-
if (
|
|
42
|
-
metadata[
|
|
41
|
+
if (inlineTokens.length > 0) {
|
|
42
|
+
metadata[CHAT_INLINE_TOKENS_METADATA_KEY] = inlineTokens;
|
|
43
43
|
}
|
|
44
44
|
if (payload.sessionMaterialization) {
|
|
45
45
|
metadata[CHAT_SESSION_MATERIALIZATION_METADATA_KEY] = payload.sessionMaterialization;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionEntryView } from "@/shared/lib/api";
|
|
1
|
+
import type { ProjectView, SessionEntryView } from "@/shared/lib/api";
|
|
2
2
|
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
3
3
|
import { getSessionProjectName } from "@/shared/lib/session-project";
|
|
4
4
|
import { t } from "@/shared/lib/i18n";
|
|
@@ -96,9 +96,20 @@ export function groupSessionsByProject(
|
|
|
96
96
|
items: NcpSessionListItemView[],
|
|
97
97
|
pinnedSessionKeys: ReadonlySet<string> = new Set(),
|
|
98
98
|
pinnedProjectRoots: ReadonlySet<string> = new Set(),
|
|
99
|
+
projects: readonly ProjectView[] = [],
|
|
99
100
|
): ChatSidebarProjectGroup[] {
|
|
100
101
|
const grouped = new Map<string, ChatSidebarProjectGroup>();
|
|
101
102
|
|
|
103
|
+
for (const project of projects) {
|
|
104
|
+
grouped.set(project.rootPath, {
|
|
105
|
+
projectRoot: project.rootPath,
|
|
106
|
+
projectName: project.name,
|
|
107
|
+
items: [],
|
|
108
|
+
latestUpdatedAt: new Date(project.updatedAt).getTime(),
|
|
109
|
+
isPinned: pinnedProjectRoots.has(project.rootPath),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
for (const item of items) {
|
|
103
114
|
const projectRoot = item.session.projectRoot?.trim();
|
|
104
115
|
if (!projectRoot) {
|
|
@@ -16,6 +16,7 @@ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.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 { useAgents } from "@/shared/hooks/use-agents";
|
|
19
|
+
import { useProjects } from "@/shared/hooks/use-projects";
|
|
19
20
|
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
20
21
|
import { normalizeSessionProjectRootValue } from "@/shared/lib/session-project";
|
|
21
22
|
|
|
@@ -55,6 +56,7 @@ export function ChatConversationWelcome({
|
|
|
55
56
|
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
56
57
|
);
|
|
57
58
|
const agentsQuery = useAgents();
|
|
59
|
+
const projectsQuery = useProjects();
|
|
58
60
|
const draftAgentId = threadSnapshot.agentId ?? selectedAgentId;
|
|
59
61
|
const availableAgents = resolveChatWelcomeAgents({
|
|
60
62
|
agents: agentsQuery.data?.agents,
|
|
@@ -80,6 +82,7 @@ export function ChatConversationWelcome({
|
|
|
80
82
|
});
|
|
81
83
|
const projectOptions = buildChatWelcomeProjectOptions({
|
|
82
84
|
defaultProjectRoot,
|
|
85
|
+
projects: projectsQuery.data?.projects ?? [],
|
|
83
86
|
sessionSummaries,
|
|
84
87
|
});
|
|
85
88
|
const selectDraftAgent = (agentId: string) => {
|
|
@@ -106,6 +106,7 @@ export function ChatWelcome({
|
|
|
106
106
|
<ChatSessionProjectDialog
|
|
107
107
|
open={isProjectDialogOpen}
|
|
108
108
|
currentProjectRoot={resolvedProjectRoot}
|
|
109
|
+
defaultWorkspacePath={defaultProjectRoot}
|
|
109
110
|
isSaving={isProjectSaving}
|
|
110
111
|
onOpenChange={setIsProjectDialogOpen}
|
|
111
112
|
onSave={saveProjectRoot}
|
|
@@ -48,4 +48,24 @@ describe("buildChatWelcomeProjectOptions", () => {
|
|
|
48
48
|
},
|
|
49
49
|
]);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
it("includes a registered project before it has any sessions", () => {
|
|
53
|
+
const options = buildChatWelcomeProjectOptions({
|
|
54
|
+
defaultProjectRoot: "/Users/demo/.nextclaw/workspace",
|
|
55
|
+
projects: [{
|
|
56
|
+
name: "Knowledge",
|
|
57
|
+
rootPath: "/tmp/knowledge",
|
|
58
|
+
template: "knowledge-base",
|
|
59
|
+
createdAt: "2026-07-15T00:00:00.000Z",
|
|
60
|
+
updatedAt: "2026-07-15T00:00:00.000Z",
|
|
61
|
+
}],
|
|
62
|
+
sessionSummaries: [],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(options).toEqual([{
|
|
66
|
+
projectRoot: "/tmp/knowledge",
|
|
67
|
+
projectName: "Knowledge",
|
|
68
|
+
sessionCount: 0,
|
|
69
|
+
}]);
|
|
70
|
+
});
|
|
51
71
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
1
|
+
import type { NcpSessionSummaryView, ProjectView } from "@/shared/lib/api";
|
|
2
2
|
import { adaptNcpSessionSummaries } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
3
3
|
import { getSessionProjectName } from "@/shared/lib/session-project";
|
|
4
4
|
|
|
@@ -17,14 +17,27 @@ function readSessionActivityAt(session: {
|
|
|
17
17
|
|
|
18
18
|
export function buildChatWelcomeProjectOptions(params: {
|
|
19
19
|
defaultProjectRoot: string | null;
|
|
20
|
+
projects?: readonly ProjectView[];
|
|
20
21
|
sessionSummaries: readonly NcpSessionSummaryView[];
|
|
21
22
|
}): ChatWelcomeProjectOption[] {
|
|
22
|
-
const { defaultProjectRoot, sessionSummaries } = params;
|
|
23
|
+
const { defaultProjectRoot, projects = [], sessionSummaries } = params;
|
|
23
24
|
const groups = new Map<
|
|
24
25
|
string,
|
|
25
26
|
ChatWelcomeProjectOption & { latestUpdatedAt: number }
|
|
26
27
|
>();
|
|
27
28
|
|
|
29
|
+
for (const project of projects) {
|
|
30
|
+
if (project.rootPath === defaultProjectRoot) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
groups.set(project.rootPath, {
|
|
34
|
+
projectRoot: project.rootPath,
|
|
35
|
+
projectName: project.name,
|
|
36
|
+
sessionCount: 0,
|
|
37
|
+
latestUpdatedAt: new Date(project.updatedAt).getTime(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
for (const session of adaptNcpSessionSummaries([...sessionSummaries])) {
|
|
29
42
|
const projectRoot = session.projectRoot?.trim();
|
|
30
43
|
if (!projectRoot || projectRoot === defaultProjectRoot) {
|
|
@@ -20,6 +20,7 @@ const destroyPresentationMock = vi.fn();
|
|
|
20
20
|
type RenderWorkspaceFilePreviewOptions = {
|
|
21
21
|
file?: Partial<ChatWorkspaceFileTab>;
|
|
22
22
|
onFileOpen?: (action: ChatFileOpenActionViewModel) => void;
|
|
23
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
23
24
|
refreshVersion?: number;
|
|
24
25
|
sessionProjectRoot?: string | null;
|
|
25
26
|
sessionWorkingDir?: string | null;
|
|
@@ -104,6 +105,7 @@ function mockTextRead(overrides: TextReadDataOverrides = {}) {
|
|
|
104
105
|
function renderWorkspaceFilePreview({
|
|
105
106
|
file,
|
|
106
107
|
onFileOpen = vi.fn(),
|
|
108
|
+
onHtmlContentHeightChange,
|
|
107
109
|
refreshVersion,
|
|
108
110
|
sessionProjectRoot = "/tmp",
|
|
109
111
|
sessionWorkingDir = "/tmp",
|
|
@@ -116,6 +118,7 @@ function renderWorkspaceFilePreview({
|
|
|
116
118
|
sessionWorkingDir={sessionWorkingDir}
|
|
117
119
|
refreshVersion={refreshVersion}
|
|
118
120
|
showBreadcrumbs={showBreadcrumbs}
|
|
121
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
119
122
|
onFileOpen={onFileOpen}
|
|
120
123
|
/>,
|
|
121
124
|
);
|
|
@@ -316,9 +319,7 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
316
319
|
});
|
|
317
320
|
|
|
318
321
|
expect(screen.getByTestId(testId)).toBeTruthy();
|
|
319
|
-
expect(
|
|
320
|
-
screen.queryByText(t("chatWorkspacePreviewTruncated")),
|
|
321
|
-
).toBeNull();
|
|
322
|
+
expect(screen.queryByText(t("chatWorkspacePreviewTruncated"))).toBeNull();
|
|
322
323
|
},
|
|
323
324
|
);
|
|
324
325
|
});
|
|
@@ -751,9 +752,7 @@ describe("ChatSessionWorkspaceFilePreview text rendering", () => {
|
|
|
751
752
|
});
|
|
752
753
|
const indexButton = screen.getByRole("button", { name: "index.ts" });
|
|
753
754
|
fireEvent.click(componentsTreeItem.querySelector("button")!);
|
|
754
|
-
expect(
|
|
755
|
-
componentsTreeItem.getAttribute("aria-expanded"),
|
|
756
|
-
).toBe("true");
|
|
755
|
+
expect(componentsTreeItem.getAttribute("aria-expanded")).toBe("true");
|
|
757
756
|
expect(onFileOpen).not.toHaveBeenCalled();
|
|
758
757
|
|
|
759
758
|
fireEvent.click(indexButton);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
3
|
+
import { WorkspaceFileContentPreview } from "@/features/chat/features/workspace/components/workspace-file-content-preview";
|
|
4
|
+
|
|
5
|
+
const defaultResizeObserver = globalThis.ResizeObserver;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.stubGlobal("ResizeObserver", defaultResizeObserver);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("reports same-origin HTML height changes without replacing the iframe", () => {
|
|
12
|
+
const observe = vi.fn();
|
|
13
|
+
const disconnect = vi.fn();
|
|
14
|
+
let notifyResize: ResizeObserverCallback = () => undefined;
|
|
15
|
+
class HeightResizeObserver {
|
|
16
|
+
constructor(callback: ResizeObserverCallback) {
|
|
17
|
+
notifyResize = callback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
observe = observe;
|
|
21
|
+
|
|
22
|
+
unobserve = vi.fn();
|
|
23
|
+
|
|
24
|
+
disconnect = disconnect;
|
|
25
|
+
}
|
|
26
|
+
vi.stubGlobal("ResizeObserver", HeightResizeObserver);
|
|
27
|
+
const onHtmlContentHeightChange = vi.fn();
|
|
28
|
+
const { unmount } = render(
|
|
29
|
+
<WorkspaceFileContentPreview
|
|
30
|
+
contentUrl="/api/server-paths/content?path=%2Ftmp%2Fexample.html"
|
|
31
|
+
kind="html"
|
|
32
|
+
label="example.html"
|
|
33
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
const frame = screen.getByTestId(
|
|
37
|
+
"workspace-html-preview",
|
|
38
|
+
) as HTMLIFrameElement;
|
|
39
|
+
const iframeDocument = document.implementation.createHTMLDocument();
|
|
40
|
+
Object.defineProperty(frame, "contentDocument", {
|
|
41
|
+
configurable: true,
|
|
42
|
+
value: iframeDocument,
|
|
43
|
+
});
|
|
44
|
+
const { documentElement } = iframeDocument;
|
|
45
|
+
Object.defineProperty(documentElement, "scrollHeight", {
|
|
46
|
+
configurable: true,
|
|
47
|
+
value: 640,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
fireEvent.load(frame);
|
|
51
|
+
|
|
52
|
+
expect(onHtmlContentHeightChange).toHaveBeenLastCalledWith(640);
|
|
53
|
+
expect(observe).toHaveBeenCalledWith(documentElement);
|
|
54
|
+
expect(screen.getByTestId("workspace-html-preview")).toBe(frame);
|
|
55
|
+
|
|
56
|
+
Object.defineProperty(documentElement, "scrollHeight", {
|
|
57
|
+
configurable: true,
|
|
58
|
+
value: 900,
|
|
59
|
+
});
|
|
60
|
+
notifyResize([], {} as ResizeObserver);
|
|
61
|
+
|
|
62
|
+
expect(onHtmlContentHeightChange).toHaveBeenLastCalledWith(900);
|
|
63
|
+
expect(screen.getByTestId("workspace-html-preview")).toBe(frame);
|
|
64
|
+
|
|
65
|
+
unmount();
|
|
66
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -180,6 +180,7 @@ function WorkspacePreviewBody({
|
|
|
180
180
|
directoryQuery,
|
|
181
181
|
fileBasePath,
|
|
182
182
|
onFileOpen,
|
|
183
|
+
onHtmlContentHeightChange,
|
|
183
184
|
previewBlock,
|
|
184
185
|
previewKind,
|
|
185
186
|
previewViewer,
|
|
@@ -194,6 +195,7 @@ function WorkspacePreviewBody({
|
|
|
194
195
|
directoryQuery: ReturnType<typeof useServerPathBrowse> | null | undefined;
|
|
195
196
|
fileBasePath: string | null;
|
|
196
197
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
198
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
197
199
|
previewBlock: ChatFileOperationBlockViewModel | null;
|
|
198
200
|
previewKind: "text" | "markdown" | "binary";
|
|
199
201
|
previewViewer: "source" | "rendered" | null;
|
|
@@ -208,6 +210,7 @@ function WorkspacePreviewBody({
|
|
|
208
210
|
contentUrl={contentUrl}
|
|
209
211
|
kind={contentUrlKind}
|
|
210
212
|
label={contentLabel}
|
|
213
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
211
214
|
/>
|
|
212
215
|
);
|
|
213
216
|
}
|
|
@@ -291,6 +294,7 @@ type ChatSessionWorkspaceFilePreviewProps = {
|
|
|
291
294
|
sessionProjectRoot: string | null;
|
|
292
295
|
sessionWorkingDir: string | null;
|
|
293
296
|
showBreadcrumbs?: boolean;
|
|
297
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
294
298
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
295
299
|
};
|
|
296
300
|
|
|
@@ -300,6 +304,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
300
304
|
sessionProjectRoot,
|
|
301
305
|
sessionWorkingDir,
|
|
302
306
|
showBreadcrumbs = true,
|
|
307
|
+
onHtmlContentHeightChange,
|
|
303
308
|
onFileOpen,
|
|
304
309
|
}: ChatSessionWorkspaceFilePreviewProps) {
|
|
305
310
|
const isPreviewMode = file.viewMode === "preview";
|
|
@@ -384,7 +389,8 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
384
389
|
previewQuery?.data?.startLine,
|
|
385
390
|
previewText,
|
|
386
391
|
]);
|
|
387
|
-
const isTextPreviewTruncated =
|
|
392
|
+
const isTextPreviewTruncated =
|
|
393
|
+
!contentUrl && Boolean(previewQuery?.data?.truncated);
|
|
388
394
|
const breadcrumbBasePath = sessionProjectRoot ?? sessionWorkingDir;
|
|
389
395
|
const breadcrumb = useMemo(
|
|
390
396
|
() =>
|
|
@@ -394,12 +400,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
394
400
|
sessionProjectRoot: breadcrumbBasePath,
|
|
395
401
|
truncated: isTextPreviewTruncated,
|
|
396
402
|
}),
|
|
397
|
-
[
|
|
398
|
-
breadcrumbBasePath,
|
|
399
|
-
isTextPreviewTruncated,
|
|
400
|
-
previewPathKind,
|
|
401
|
-
resolvedPath,
|
|
402
|
-
],
|
|
403
|
+
[breadcrumbBasePath, isTextPreviewTruncated, previewPathKind, resolvedPath],
|
|
403
404
|
);
|
|
404
405
|
|
|
405
406
|
return (
|
|
@@ -422,6 +423,7 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
422
423
|
directoryQuery={directoryQuery}
|
|
423
424
|
fileBasePath={sessionWorkingDir}
|
|
424
425
|
onFileOpen={onFileOpen}
|
|
426
|
+
onHtmlContentHeightChange={onHtmlContentHeightChange}
|
|
425
427
|
previewBlock={previewBlock}
|
|
426
428
|
previewKind={previewKind}
|
|
427
429
|
previewViewer={previewViewer}
|