@nextclaw/ui 0.15.0 → 0.15.1
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 +19 -0
- package/dist/assets/{api-DLtsiBKm.js → api-b1R9VdeH.js} +1 -1
- package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-D2O9pch9.js} +1 -1
- package/dist/assets/{book-open-BQwWBDKb.js → book-open-B77lQ2Tg.js} +1 -1
- package/dist/assets/{channels-list-page-Cv8o9Hxg.js → channels-list-page-DcsIQc6A.js} +2 -2
- package/dist/assets/chat-page-CZ7WxPir.js +1 -0
- package/dist/assets/{config-split-page-EidRBpP2.js → config-split-page-BWQ6_Raq.js} +1 -1
- package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-C-NXSioX.js} +1 -1
- package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
- package/dist/assets/{dist-CBxWOazz.js → dist-YtoqU-AT.js} +1 -1
- package/dist/assets/{dist-D2eZC3OR.js → dist-jdGmbb6w.js} +1 -1
- package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
- package/dist/assets/{doc-browser-B76XOXmW.js → doc-browser-BdZPFOVa.js} +1 -1
- package/dist/assets/{doc-browser-context-Sv-044Cl.js → doc-browser-context-DwokemUn.js} +1 -1
- package/dist/assets/{es2015-DhQo_oS6.js → es2015-bNpL2LKy.js} +1 -1
- package/dist/assets/{external-link-CM5dnUI6.js → external-link-BM6fk8q-.js} +1 -1
- package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-CntWolRA.js} +1 -1
- package/dist/assets/index-BIcEw90u.js +104 -0
- package/dist/assets/index-BiNSTKTU.css +1 -0
- package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
- package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-CLmUstLN.js} +1 -1
- package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
- package/dist/assets/{model-config-page-Cdr9P6TQ.js → model-config-page-Di0ySJD2.js} +1 -1
- package/dist/assets/plus-vDqW5Kxp.js +1 -0
- package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-Cm_a4GHf.js} +1 -1
- package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
- package/dist/assets/{react-DjpzoZXB.js → react-BN8uzPnp.js} +1 -1
- package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-BpVcFPmJ.js} +1 -1
- package/dist/assets/remote-CLQ0IBM8.js +1 -0
- package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
- package/dist/assets/{runtime-config-page-CfTqh1nv.js → runtime-config-page-CflvvoDc.js} +1 -1
- package/dist/assets/{save-BZof1uie.js → save-BpFW5fpk.js} +1 -1
- package/dist/assets/{search-MxAarS_g.js → search-D0uO7enf.js} +1 -1
- package/dist/assets/{search-config-page-Dm8daYny.js → search-config-page-D0klFUXd.js} +1 -1
- package/dist/assets/{secrets-config-page-D8AbcR05.js → secrets-config-page-dVZ631Wp.js} +1 -1
- package/dist/assets/security-config-C5gnlkuj.js +1 -0
- package/dist/assets/{select-D7GcNP0z.js → select-Bh-CrA4p.js} +1 -1
- package/dist/assets/{setting-row-CuozlnJd.js → setting-row-BJfCHRcb.js} +1 -1
- package/dist/assets/settings-2-Blzi1vr6.js +1 -0
- package/dist/assets/{side-dock-BcLau5S_.js → side-dock-ZdaSrQVB.js} +1 -1
- package/dist/assets/{skeleton-shfmXuYp.js → skeleton-BLlEzebZ.js} +1 -1
- package/dist/assets/{switch-C1BWMU5c.js → switch-CFLJFNSd.js} +1 -1
- package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-CzGihiQx.js} +1 -1
- package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CdcrBjrC.js} +1 -1
- package/dist/assets/{use-config-DAdbT5QJ.js → use-config-CVPgsBmU.js} +1 -1
- package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CSeBk9SF.js} +1 -1
- package/dist/assets/x-BF2KQ--V.js +1 -0
- package/dist/index.html +26 -26
- package/package.json +9 -9
- package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
- package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
- package/src/features/agents/components/agent-details-dialog.tsx +398 -0
- package/src/features/agents/components/agent-dialogs.tsx +20 -47
- package/src/features/agents/components/agents-page.tsx +76 -10
- package/src/features/agents/types/agent-form.types.ts +21 -0
- package/src/features/agents/utils/agent-form.utils.ts +39 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +34 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +1 -7
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
- package/src/features/chat/managers/chat-thread.manager.ts +8 -2
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
- package/src/features/system-status/components/desktop-update-config.tsx +91 -2
- package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
- package/src/shared/lib/api/ncp-session.types.ts +1 -1
- package/src/shared/lib/api/types.ts +12 -3
- package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +3 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
- package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
- package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
- package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
- package/dist/assets/ellipsis-2uzhypw5.js +0 -1
- package/dist/assets/index-CBRP-7-a.css +0 -1
- package/dist/assets/index-Z1fYpbNd.js +0 -104
- package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
- package/dist/assets/plus-3ztRZD_l.js +0 -1
- package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
- package/dist/assets/remote-CKWZfVVB.js +0 -1
- package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
- package/dist/assets/security-config-Ds0LsJmo.js +0 -1
- package/dist/assets/x-DqL1mYLC.js +0 -1
package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
ChevronDown,
|
|
5
|
+
Loader2,
|
|
6
|
+
MessageSquareText,
|
|
7
|
+
Search,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
|
|
10
|
+
import {
|
|
11
|
+
useNcpSessionListView,
|
|
12
|
+
type NcpSessionListItemView,
|
|
13
|
+
} from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
14
|
+
import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
|
|
15
|
+
import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
|
|
16
|
+
import {
|
|
17
|
+
formatSessionListTime,
|
|
18
|
+
sessionActivityPreviewText,
|
|
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";
|
|
31
|
+
import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
|
|
32
|
+
import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
|
|
33
|
+
import {
|
|
34
|
+
Popover,
|
|
35
|
+
PopoverContent,
|
|
36
|
+
PopoverTrigger,
|
|
37
|
+
} from "@/shared/components/ui/popover";
|
|
38
|
+
import { Input } from "@/shared/components/ui/input";
|
|
39
|
+
import { t } from "@/shared/lib/i18n";
|
|
40
|
+
import { cn } from "@/shared/lib/utils";
|
|
41
|
+
|
|
42
|
+
const SWITCHER_TRIGGER_CLASS =
|
|
43
|
+
"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-2 focus-visible:ring-primary/40";
|
|
44
|
+
|
|
45
|
+
function ChatSessionTitle({ title }: { title: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
48
|
+
{title}
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ChatSessionTitleSwitcherEmptyState({ label }: { label: string }) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex items-center gap-2 px-3 py-4 text-xs text-muted-foreground">
|
|
56
|
+
<MessageSquareText className="h-4 w-4 shrink-0 text-muted-foreground/55" />
|
|
57
|
+
<span>{label}</span>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ChatSessionTitleSwitcherLoadingState() {
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex items-center gap-2 px-3 py-4 text-xs text-muted-foreground">
|
|
65
|
+
<Loader2 className="h-4 w-4 shrink-0 animate-spin" />
|
|
66
|
+
<span>{t("sessionsLoading")}</span>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
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-2 focus-visible:ring-primary/40",
|
|
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
|
+
function ChatSessionTitleSwitcherPopover({
|
|
162
|
+
selectedSessionKey,
|
|
163
|
+
title,
|
|
164
|
+
}: {
|
|
165
|
+
selectedSessionKey: string | null;
|
|
166
|
+
title: string;
|
|
167
|
+
}) {
|
|
168
|
+
const presenter = usePresenter();
|
|
169
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
170
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
171
|
+
const optimisticReadAtBySessionKey = useChatSessionListStore(
|
|
172
|
+
(state) => state.optimisticReadAtBySessionKey,
|
|
173
|
+
);
|
|
174
|
+
const sessionTypesData = useChatQueryStore(
|
|
175
|
+
(state) => state.snapshot.sessionTypesQuery?.data ?? null,
|
|
176
|
+
);
|
|
177
|
+
const { isLoading, items } = useNcpSessionListView({ query: searchQuery });
|
|
178
|
+
const sortedItems = useMemo(
|
|
179
|
+
() => sortSessionItemsByActivityAtDesc(items),
|
|
180
|
+
[items],
|
|
181
|
+
);
|
|
182
|
+
const sessionTypeOptions = useMemo(
|
|
183
|
+
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
184
|
+
[sessionTypesData?.options],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const selectSession = (sessionKey: string) => {
|
|
188
|
+
setIsOpen(false);
|
|
189
|
+
setSearchQuery("");
|
|
190
|
+
if (sessionKey === selectedSessionKey) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
presenter.chatSessionListManager.selectSession(sessionKey);
|
|
194
|
+
};
|
|
195
|
+
const updateOpen = (open: boolean) => {
|
|
196
|
+
setIsOpen(open);
|
|
197
|
+
if (!open) {
|
|
198
|
+
setSearchQuery("");
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<Popover open={isOpen} onOpenChange={updateOpen}>
|
|
204
|
+
<PopoverTrigger asChild>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
aria-label={`${t("chatSessionSwitch")}: ${title}`}
|
|
208
|
+
aria-expanded={isOpen}
|
|
209
|
+
className={SWITCHER_TRIGGER_CLASS}
|
|
210
|
+
>
|
|
211
|
+
<span className="min-w-0 truncate">{title}</span>
|
|
212
|
+
<ChevronDown
|
|
213
|
+
className={cn(
|
|
214
|
+
"h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform group-hover:text-accent-foreground",
|
|
215
|
+
isOpen ? "rotate-180" : null,
|
|
216
|
+
)}
|
|
217
|
+
/>
|
|
218
|
+
</button>
|
|
219
|
+
</PopoverTrigger>
|
|
220
|
+
<PopoverContent
|
|
221
|
+
align="start"
|
|
222
|
+
className="w-[22rem] max-w-[calc(100vw-2rem)] p-0"
|
|
223
|
+
>
|
|
224
|
+
<div className="space-y-2 border-b border-border px-3 py-2">
|
|
225
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
|
|
226
|
+
{t("chatSessionSwitcherTitle")}
|
|
227
|
+
</div>
|
|
228
|
+
<div className="relative">
|
|
229
|
+
<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" />
|
|
230
|
+
<Input
|
|
231
|
+
value={searchQuery}
|
|
232
|
+
onChange={(event) => setSearchQuery(event.target.value)}
|
|
233
|
+
placeholder={t("chatSearchSessionPlaceholder")}
|
|
234
|
+
aria-label={t("chatSearchSessionPlaceholder")}
|
|
235
|
+
className="h-8 rounded-lg bg-background pl-8 pr-2 text-xs"
|
|
236
|
+
autoFocus
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="max-h-80 overflow-y-auto p-1.5">
|
|
241
|
+
{isLoading ? (
|
|
242
|
+
<ChatSessionTitleSwitcherLoadingState />
|
|
243
|
+
) : sortedItems.length > 0 ? (
|
|
244
|
+
<div className="space-y-1">
|
|
245
|
+
{sortedItems.map((item) => (
|
|
246
|
+
<ChatSessionSwitchItem
|
|
247
|
+
key={item.session.key}
|
|
248
|
+
item={item}
|
|
249
|
+
selectedSessionKey={selectedSessionKey}
|
|
250
|
+
optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
|
|
251
|
+
sessionTypeOptions={sessionTypeOptions}
|
|
252
|
+
onSelect={selectSession}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
) : (
|
|
257
|
+
<ChatSessionTitleSwitcherEmptyState
|
|
258
|
+
label={
|
|
259
|
+
searchQuery.trim()
|
|
260
|
+
? t("chatSessionSwitcherNoResults")
|
|
261
|
+
: t("sessionsEmpty")
|
|
262
|
+
}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</PopoverContent>
|
|
267
|
+
</Popover>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function ChatSessionTitleSwitcher({
|
|
272
|
+
layoutMode,
|
|
273
|
+
selectedSessionKey,
|
|
274
|
+
title,
|
|
275
|
+
}: {
|
|
276
|
+
layoutMode: "desktop" | "mobile";
|
|
277
|
+
selectedSessionKey: string | null;
|
|
278
|
+
title: string;
|
|
279
|
+
}) {
|
|
280
|
+
const isSidebarCollapsed = useViewportLayoutStore(
|
|
281
|
+
(state) => state.isSidebarCollapsed,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (layoutMode !== "desktop" || !isSidebarCollapsed) {
|
|
285
|
+
return <ChatSessionTitle title={title} />;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<ChatSessionTitleSwitcherPopover
|
|
290
|
+
selectedSessionKey={selectedSessionKey}
|
|
291
|
+
title={title}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
}
|
package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
sessionActivityPreviewText,
|
|
6
6
|
sessionDisplayName,
|
|
7
7
|
sessionMatchesQuery
|
|
8
|
-
} from '
|
|
8
|
+
} from '@/features/chat/features/session/utils/chat-session-display.utils';
|
|
9
9
|
|
|
10
10
|
function createSession(overrides: Partial<SessionEntryView> = {}): SessionEntryView {
|
|
11
11
|
return {
|
|
@@ -88,6 +88,20 @@ describe('chat-session-display', () => {
|
|
|
88
88
|
).toBe('最终回复内容');
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
it('does not show cancelled activity as ordinary session preview text', () => {
|
|
92
|
+
expect(
|
|
93
|
+
sessionActivityPreviewText(
|
|
94
|
+
createSession({
|
|
95
|
+
activityPreview: {
|
|
96
|
+
state: 'cancelled',
|
|
97
|
+
statusText: 'Run interrupted: User stopped the current run.',
|
|
98
|
+
timestamp: '2026-05-16T01:00:00.000Z'
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
91
105
|
it('formats today activity as time like WeChat session lists', () => {
|
|
92
106
|
expect(
|
|
93
107
|
formatSessionListTime(
|
package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts
CHANGED
|
@@ -105,8 +105,8 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
105
105
|
metadata: {
|
|
106
106
|
last_activity_preview: {
|
|
107
107
|
state: 'completed',
|
|
108
|
-
replyText: '
|
|
109
|
-
statusText: '
|
|
108
|
+
replyText: 'Plan is ready',
|
|
109
|
+
statusText: 'Tool call completed',
|
|
110
110
|
timestamp: '2026-05-16T01:00:00.000Z',
|
|
111
111
|
},
|
|
112
112
|
},
|
|
@@ -115,8 +115,28 @@ describe('adaptNcpSessionSummary', () => {
|
|
|
115
115
|
|
|
116
116
|
expect(adapted.activityPreview).toEqual({
|
|
117
117
|
state: 'completed',
|
|
118
|
-
replyText: '
|
|
119
|
-
statusText: '
|
|
118
|
+
replyText: 'Plan is ready',
|
|
119
|
+
statusText: 'Tool call completed',
|
|
120
|
+
timestamp: '2026-05-16T01:00:00.000Z',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('maps cancelled session activity preview metadata into the session entry', () => {
|
|
125
|
+
const adapted = adaptNcpSessionSummary(
|
|
126
|
+
createSummary({
|
|
127
|
+
metadata: {
|
|
128
|
+
last_activity_preview: {
|
|
129
|
+
state: 'cancelled',
|
|
130
|
+
statusText: 'Run interrupted: User stopped the current run.',
|
|
131
|
+
timestamp: '2026-05-16T01:00:00.000Z',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(adapted.activityPreview).toEqual({
|
|
138
|
+
state: 'cancelled',
|
|
139
|
+
statusText: 'Run interrupted: User stopped the current run.',
|
|
120
140
|
timestamp: '2026-05-16T01:00:00.000Z',
|
|
121
141
|
});
|
|
122
142
|
});
|
|
@@ -15,6 +15,9 @@ export function sessionActivityPreviewText(session: SessionEntryView): string |
|
|
|
15
15
|
if (!preview) {
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
|
+
if (preview.state === 'cancelled') {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
18
21
|
if (preview.state === 'failed' || preview.state === 'running') {
|
|
19
22
|
return preview.statusText ?? preview.replyText ?? null;
|
|
20
23
|
}
|
|
@@ -189,6 +189,23 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
189
189
|
).toBe("js");
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
it("keeps HTML files in the source preview when preview viewer is automatic", () => {
|
|
193
|
+
mockTextRead({
|
|
194
|
+
resolvedPath: "/tmp/example.html",
|
|
195
|
+
text: "<!doctype html><h1>Hello</h1>",
|
|
196
|
+
});
|
|
197
|
+
renderWorkspaceFilePreview({
|
|
198
|
+
file: {
|
|
199
|
+
label: "example.html",
|
|
200
|
+
path: "/tmp/example.html",
|
|
201
|
+
viewMode: "preview",
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(screen.queryByTestId("workspace-html-preview")).toBeNull();
|
|
206
|
+
expect(screen.getByTestId("file-code-surface")).toBeTruthy();
|
|
207
|
+
});
|
|
208
|
+
|
|
192
209
|
it("renders HTML files through an unrestricted server content iframe when rendered preview is requested", () => {
|
|
193
210
|
mockTextRead({
|
|
194
211
|
resolvedPath: "/tmp/example.html",
|
|
@@ -212,6 +229,23 @@ describe("ChatSessionWorkspaceFilePreview rendering", () => {
|
|
|
212
229
|
expect(screen.queryByTestId("file-code-surface")).toBeNull();
|
|
213
230
|
});
|
|
214
231
|
|
|
232
|
+
it("keeps HTML files in the source preview when source is requested", () => {
|
|
233
|
+
mockTextRead({
|
|
234
|
+
resolvedPath: "/tmp/example.html",
|
|
235
|
+
text: "<!doctype html><h1>Hello</h1>",
|
|
236
|
+
});
|
|
237
|
+
renderWorkspaceFilePreview({
|
|
238
|
+
file: {
|
|
239
|
+
path: "/tmp/example.html",
|
|
240
|
+
previewViewer: "source",
|
|
241
|
+
viewMode: "preview",
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(screen.queryByTestId("workspace-html-preview")).toBeNull();
|
|
246
|
+
expect(screen.getByTestId("file-code-surface")).toBeTruthy();
|
|
247
|
+
});
|
|
248
|
+
|
|
215
249
|
it("falls back to the source preview when rendered is requested for a non-HTML file", () => {
|
|
216
250
|
mockTextRead({ resolvedPath: "/tmp/example.txt", text: "plain text" });
|
|
217
251
|
renderWorkspaceFilePreview({
|
package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx
CHANGED
|
@@ -24,17 +24,11 @@ function inferPreviewKind(params: {
|
|
|
24
24
|
return /\.mdx?$/i.test(params.path) ? "markdown" : "text";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function isHtmlFilePath(path: string): boolean {
|
|
28
|
-
return /\.html?$/i.test(path);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
function resolveFilePreviewViewer(params: {
|
|
32
28
|
path: string;
|
|
33
29
|
viewer?: ChatFilePreviewViewer | null;
|
|
34
30
|
}): "source" | "rendered" {
|
|
35
|
-
return params.viewer === "rendered" &&
|
|
36
|
-
? "rendered"
|
|
37
|
-
: "source";
|
|
31
|
+
return params.viewer === "rendered" && /\.html?$/i.test(params.path) ? "rendered" : "source";
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
function buildPreviewBlock(params: {
|
|
@@ -158,4 +158,50 @@ describe("buildWorkspaceTabsViewModel", () => {
|
|
|
158
158
|
active: true,
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
it("keeps distinct source and rendered file tabs without collapsing by path", () => {
|
|
163
|
+
const renderedTab = {
|
|
164
|
+
key: "parent::preview:rendered::demo.html",
|
|
165
|
+
parentSessionKey: "parent-1",
|
|
166
|
+
path: "demo.html",
|
|
167
|
+
label: "demo.html",
|
|
168
|
+
viewMode: "preview" as const,
|
|
169
|
+
previewViewer: "rendered" as const,
|
|
170
|
+
};
|
|
171
|
+
const sourceTab = {
|
|
172
|
+
key: "parent::preview::demo.html",
|
|
173
|
+
parentSessionKey: "parent-1",
|
|
174
|
+
path: "demo.html",
|
|
175
|
+
label: "demo.html",
|
|
176
|
+
viewMode: "preview" as const,
|
|
177
|
+
previewViewer: "source" as const,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const tabs = buildWorkspaceTabsViewModel({
|
|
181
|
+
resolvedChildTabs: [],
|
|
182
|
+
activeSideChatDraft: null,
|
|
183
|
+
workspaceFileTabs: [sourceTab, renderedTab],
|
|
184
|
+
sessionCronJobCount: 0,
|
|
185
|
+
activeSelection: {
|
|
186
|
+
kind: "file",
|
|
187
|
+
file: sourceTab,
|
|
188
|
+
},
|
|
189
|
+
optimisticReadAtBySessionKey: {},
|
|
190
|
+
onSelectSession: vi.fn(),
|
|
191
|
+
onSelectFile: vi.fn(),
|
|
192
|
+
onCloseFile: vi.fn(),
|
|
193
|
+
onSelectCronJobs: vi.fn(),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(tabs).toEqual([
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
key: "file:parent::preview::demo.html",
|
|
199
|
+
active: true,
|
|
200
|
+
}),
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
key: "file:parent::preview:rendered::demo.html",
|
|
203
|
+
active: false,
|
|
204
|
+
}),
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
161
207
|
});
|
|
@@ -331,6 +331,53 @@ describe('ChatThreadManager', () => {
|
|
|
331
331
|
});
|
|
332
332
|
|
|
333
333
|
describe('ChatThreadManager workspace file tabs', () => {
|
|
334
|
+
it('opens HTML previews in source mode when the viewer is omitted or auto', () => {
|
|
335
|
+
const manager = new ChatThreadManager(
|
|
336
|
+
createUiManager(),
|
|
337
|
+
{} as ConstructorParameters<typeof ChatThreadManager>[1],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
manager.openFilePreview({
|
|
341
|
+
path: 'demo.html',
|
|
342
|
+
label: 'demo.html',
|
|
343
|
+
viewMode: 'preview',
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
347
|
+
activeWorkspaceFileKey: 'parent-session-1::preview::demo.html',
|
|
348
|
+
workspaceFileTabs: [
|
|
349
|
+
{
|
|
350
|
+
key: 'parent-session-1::preview::demo.html',
|
|
351
|
+
path: 'demo.html',
|
|
352
|
+
viewMode: 'preview',
|
|
353
|
+
previewViewer: 'source',
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
manager.openFilePreview({
|
|
359
|
+
path: 'auto.html',
|
|
360
|
+
label: 'auto.html',
|
|
361
|
+
viewMode: 'preview',
|
|
362
|
+
previewViewer: 'auto',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(useChatThreadStore.getState().snapshot).toMatchObject({
|
|
366
|
+
activeWorkspaceFileKey: 'parent-session-1::preview::auto.html',
|
|
367
|
+
workspaceFileTabs: [
|
|
368
|
+
{
|
|
369
|
+
key: 'parent-session-1::preview::auto.html',
|
|
370
|
+
path: 'auto.html',
|
|
371
|
+
viewMode: 'preview',
|
|
372
|
+
previewViewer: 'source',
|
|
373
|
+
},
|
|
374
|
+
expect.objectContaining({
|
|
375
|
+
key: 'parent-session-1::preview::demo.html',
|
|
376
|
+
}),
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
334
381
|
it('keeps source and rendered previews for the same file as separate workspace tabs', () => {
|
|
335
382
|
const manager = new ChatThreadManager(
|
|
336
383
|
createUiManager(),
|
|
@@ -111,14 +111,20 @@ export class ChatThreadManager {
|
|
|
111
111
|
return null;
|
|
112
112
|
}
|
|
113
113
|
const normalizedParentSessionKey = parentSessionKey?.trim() || null;
|
|
114
|
-
const
|
|
114
|
+
const previewViewer =
|
|
115
|
+
action.viewMode === 'preview' &&
|
|
116
|
+
action.previewViewer !== 'rendered' &&
|
|
117
|
+
(action.previewViewer === 'auto' || action.previewViewer === 'source' || /\.html?$/i.test(normalizedPath))
|
|
118
|
+
? 'source'
|
|
119
|
+
: action.previewViewer ?? null;
|
|
120
|
+
const tabViewIdentity = action.viewMode === 'preview' && previewViewer === 'rendered' ? 'preview:rendered' : action.viewMode;
|
|
115
121
|
return {
|
|
116
122
|
key: `${normalizedParentSessionKey ?? 'draft'}::${tabViewIdentity}::${normalizedPath}`,
|
|
117
123
|
parentSessionKey: normalizedParentSessionKey,
|
|
118
124
|
path: normalizedPath,
|
|
119
125
|
label: action.label?.trim() || null,
|
|
120
126
|
viewMode: action.viewMode,
|
|
121
|
-
previewViewer
|
|
127
|
+
previewViewer,
|
|
122
128
|
line: action.line ?? null,
|
|
123
129
|
column: action.column ?? null,
|
|
124
130
|
rawText: action.rawText ?? null,
|