@nextclaw/ui 0.12.25 → 0.12.27
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 +74 -0
- package/dist/assets/{channels-list-page-FJDuPwU6.js → channels-list-page-DkPvpAqc.js} +1 -1
- package/dist/assets/chat-page-b7Zf32fF.js +1 -0
- package/dist/assets/{desktop-kk7qvZ-v.js → desktop-DVUbOWbR.js} +1 -1
- package/dist/assets/index-DmWo8dX2.css +1 -0
- package/dist/assets/{index-D-AAMKCt.js → index-DqJ3CYwi.js} +34 -37
- package/dist/assets/marketplace-page-BVqFjnEB.js +105 -0
- package/dist/assets/marketplace-page-DkQ2hTs1.js +1 -0
- package/dist/assets/{mcp-marketplace-page-DIq_SpMe.js → mcp-marketplace-page-BOYJO0kp.js} +1 -1
- package/dist/assets/mcp-marketplace-page-DSML7NN0.js +1 -0
- package/dist/assets/{model-config-Bc6VVnxy.js → model-config-Bg2yycmn.js} +1 -1
- package/dist/assets/{providers-list-DN0tvISH.js → providers-list-DC1q3fvC.js} +1 -1
- package/dist/assets/{runtime-config-page-CRWOwBbl.js → runtime-config-page-q-nC0C5i.js} +1 -1
- package/dist/assets/{search-config-C4c1yZSP.js → search-config-CcKHif8O.js} +1 -1
- package/dist/assets/{secrets-config-zAF30YfO.js → secrets-config-DSg6O92a.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-Cvz8ZteY.js → use-infinite-scroll-loader-DF2e6nQ2.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +9 -9
- package/src/features/agents/components/agents-page.test.tsx +1 -1
- package/src/features/agents/components/agents-page.tsx +1 -1
- package/src/features/chat/components/chat-session-workspace-panel.tsx +31 -45
- package/src/features/chat/components/chat-sidebar-session-item.tsx +7 -9
- package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +5 -2
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +2 -2
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +106 -78
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +172 -167
- package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
- package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +2 -2
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
- package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +147 -88
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -0
- package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
- package/src/features/chat/managers/ncp-chat-presenter.manager.ts +1 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +4 -1
- package/src/features/chat/stores/chat-input.store.ts +3 -1
- package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +54 -16
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +96 -24
- package/src/features/marketplace/components/marketplace-page.test.tsx +4 -0
- package/src/features/marketplace/components/marketplace-page.tsx +16 -12
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +14 -5
- package/src/platforms/desktop/components/desktop-app-shell.test.tsx +1 -0
- package/src/platforms/desktop/components/desktop-app-shell.tsx +1 -1
- package/dist/assets/chat-page-D1fMNBrT.js +0 -1
- package/dist/assets/index-DnBeV2Xm.css +0 -1
- package/dist/assets/marketplace-page-BrCLRIc4.js +0 -105
- package/dist/assets/marketplace-page-odDpPYEs.js +0 -1
- package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as e,m as t,p as n,r}from"./i18n-D1144VAA.js";import{$t as i,Bt as a,Qt as o,Zt as s,en as c}from"./api-DGD9_Bg4.js";import{t as l}from"./createLucideIcon-DzY6wN61.js";import{a as u,i as d,n as f,r as p,t as m}from"./select-BUTwE_lC.js";import{
|
|
1
|
+
import{_ as e,m as t,p as n,r}from"./i18n-D1144VAA.js";import{$t as i,Bt as a,Qt as o,Zt as s,en as c}from"./api-DGD9_Bg4.js";import{t as l}from"./createLucideIcon-DzY6wN61.js";import{a as u,i as d,n as f,r as p,t as m}from"./select-BUTwE_lC.js";import{u as h}from"./index-DqJ3CYwi.js";var g=class extends c{constructor(e,t){super(e,t)}bindMethods(){super.bindMethods(),this.fetchNextPage=this.fetchNextPage.bind(this),this.fetchPreviousPage=this.fetchPreviousPage.bind(this)}setOptions(e){super.setOptions({...e,behavior:i()})}getOptimisticResult(e){return e.behavior=i(),super.getOptimisticResult(e)}fetchNextPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`forward`}}})}fetchPreviousPage(e){return this.fetch({...e,meta:{fetchMore:{direction:`backward`}}})}createResult(e,t){let{state:n}=e,r=super.createResult(e,t),{isFetching:i,isRefetching:a,isError:c,isRefetchError:l}=r,u=n.fetchMeta?.fetchMore?.direction,d=c&&u===`forward`,f=i&&u===`forward`,p=c&&u===`backward`,m=i&&u===`backward`;return{...r,fetchNextPage:this.fetchNextPage,fetchPreviousPage:this.fetchPreviousPage,hasNextPage:s(t,n.data),hasPreviousPage:o(t,n.data),isFetchNextPageError:d,isFetchingNextPage:f,isFetchPreviousPageError:p,isFetchingPreviousPage:m,isRefetchError:l&&!d&&!p,isRefetching:a&&!f&&!m}}};function _(e,t){return a(e,g,t)}var v=l(`PackageSearch`,[[`path`,{d:`M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14`,key:`e7tb2h`}],[`path`,{d:`m7.5 4.27 9 5.15`,key:`1c824w`}],[`polyline`,{points:`3.29 7 12 12 20.71 7`,key:`ousv84`}],[`line`,{x1:`12`,x2:`12`,y1:`22`,y2:`12`,key:`a4e8g8`}],[`circle`,{cx:`18.5`,cy:`15.5`,r:`2.5`,key:`b5zd12`}],[`path`,{d:`M20.27 17.27 22 19`,key:`1l4muz`}]]);function y(e){if(!e||e.pages.length===0)return;let t=e.pages.flatMap(e=>e.items);return{...e.pages[e.pages.length-1],items:t,pages:e.pages,loadedItems:t.length,loadedPages:e.pages.length}}var b=n();function x({scope:e,searchText:t,searchPlaceholder:n,sort:i,onSearchTextChange:a,onSortChange:o}){return(0,b.jsx)(`div`,{className:`mb-4`,children:(0,b.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,b.jsxs)(`div`,{className:`relative min-w-0 flex-1`,children:[(0,b.jsx)(v,{className:`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400`}),(0,b.jsx)(`input`,{value:t,onChange:e=>a(e.target.value),placeholder:n,className:`h-9 w-full rounded-xl border border-gray-200/80 pl-9 pr-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40`})]}),e===`all`&&(0,b.jsxs)(m,{value:i,onValueChange:e=>o(e),children:[(0,b.jsx)(d,{className:`h-9 w-[150px] shrink-0 rounded-lg`,children:(0,b.jsx)(u,{})}),(0,b.jsxs)(f,{children:[(0,b.jsx)(p,{value:`relevance`,children:r(`marketplaceSortRelevance`)}),(0,b.jsx)(p,{value:`updated`,children:r(`marketplaceSortUpdated`)})]})]})]})})}function S({count:e}){return(0,b.jsx)(b.Fragment,{children:Array.from({length:e},(e,t)=>(0,b.jsx)(`article`,{className:`h-full rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm`,children:(0,b.jsxs)(`div`,{className:`flex items-start justify-between gap-3.5`,children:[(0,b.jsxs)(`div`,{className:`flex min-w-0 flex-1 gap-3`,children:[(0,b.jsx)(h,{className:`h-10 w-10 shrink-0 rounded-xl`}),(0,b.jsxs)(`div`,{className:`min-w-0 flex-1 space-y-2 pt-0.5`,children:[(0,b.jsx)(h,{className:`h-4 w-32 max-w-[70%]`}),(0,b.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,b.jsx)(h,{className:`h-3 w-12`}),(0,b.jsx)(h,{className:`h-3 w-24`})]}),(0,b.jsx)(h,{className:`h-3 w-full`})]})]}),(0,b.jsx)(h,{className:`h-8 w-20 shrink-0 rounded-xl`})]})},`marketplace-skeleton-${t}`))})}function C({hasMore:e,loading:t,sentinelRef:n}){return!e&&!t?null:(0,b.jsxs)(`div`,{className:`py-4`,children:[e&&(0,b.jsx)(`div`,{ref:n,className:`h-1 w-full`,"aria-hidden":`true`}),t&&(0,b.jsx)(`div`,{"data-testid":`marketplace-loading-more`,className:`pt-3 text-center text-xs text-gray-500`,children:r(`loading`)})]})}function w(e){let t=e.trim().toLowerCase().replace(/_/g,`-`),n=[t,t.split(`-`)[0],`en`];return Array.from(new Set(n.filter(Boolean)))}function T(e){return e.trim().toLowerCase().replace(/_/g,`-`)}function E(e,t,n){if(e){let t=Object.entries(e).map(([e,t])=>({locale:T(e),text:typeof t==`string`?t.trim():``})).filter(e=>e.text.length>0);if(t.length>0){let e=new Map(t.map(e=>[e.locale,e.text]));for(let t of n){let n=T(t),r=e.get(n);if(r)return r}for(let e of n){let n=T(e).split(`-`)[0];if(!n)continue;let r=t.find(e=>e.locale===n||e.locale.startsWith(`${n}-`));if(r)return r.text}return t[0]?.text??``}}return t?.trim()??``}function D(e,t){if(!e)return``;for(let n of t)if(T(n).split(`-`)[0]===`zh`&&e.descriptionZh?.trim())return e.descriptionZh.trim();return e.description?.trim()?e.description.trim():e.descriptionZh?.trim()?e.descriptionZh.trim():``}var O=e(t(),1),k=160;function A(e){let t=(0,O.useRef)(null),n=(0,O.useRef)(null),r=(0,O.useRef)(e.onLoadMore),i=(0,O.useRef)(!1);return(0,O.useEffect)(()=>{r.current=e.onLoadMore},[e.onLoadMore]),(0,O.useEffect)(()=>{e.disabled&&(i.current=!1)},[e.disabled]),(0,O.useEffect)(()=>{let a=t.current,o=n.current,s=e.thresholdPx??k;if(e.disabled||!a||!o)return;let c=()=>{i.current||e.disabled||(i.current=!0,Promise.resolve(r.current()).finally(()=>{i.current=!1}))},l=()=>{o.getBoundingClientRect().top-a.getBoundingClientRect().bottom<=s&&c()};if(typeof IntersectionObserver==`function`){let e=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&c()},{root:a,rootMargin:`0px 0px ${s}px 0px`});return e.observe(o),l(),()=>{e.disconnect()}}return a.addEventListener(`scroll`,l,{passive:!0}),l(),()=>{a.removeEventListener(`scroll`,l)}},[e.disabled,e.thresholdPx,e.watchValue]),{containerRef:t,sentinelRef:n}}export{x as a,y as c,E as i,_ as l,w as n,C as o,D as r,S as s,A as t};
|
package/dist/index.html
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
})();
|
|
79
79
|
</script>
|
|
80
80
|
<title>NextClaw</title>
|
|
81
|
-
<script type="module" crossorigin src="/assets/index-
|
|
81
|
+
<script type="module" crossorigin src="/assets/index-DqJ3CYwi.js"></script>
|
|
82
82
|
<link rel="modulepreload" crossorigin href="/assets/i18n-D1144VAA.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DzY6wN61.js">
|
|
84
84
|
<link rel="modulepreload" crossorigin href="/assets/cpu-DPPwMzoC.js">
|
|
@@ -105,8 +105,8 @@
|
|
|
105
105
|
<link rel="modulepreload" crossorigin href="/assets/doc-browser-rZIQIjuw.js">
|
|
106
106
|
<link rel="modulepreload" crossorigin href="/assets/doc-browser-CAhfnm0D.js">
|
|
107
107
|
<link rel="modulepreload" crossorigin href="/assets/use-config-Cyv5IuSt.js">
|
|
108
|
-
<link rel="modulepreload" crossorigin href="/assets/desktop-
|
|
109
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
108
|
+
<link rel="modulepreload" crossorigin href="/assets/desktop-DVUbOWbR.js">
|
|
109
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DmWo8dX2.css">
|
|
110
110
|
</head>
|
|
111
111
|
|
|
112
112
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.27",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
"tailwind-merge": "^2.5.4",
|
|
29
29
|
"zod": "^3.23.8",
|
|
30
30
|
"zustand": "^5.0.2",
|
|
31
|
-
"@nextclaw/agent-chat": "0.1.
|
|
32
|
-
"@nextclaw/agent-chat-ui": "0.3.
|
|
33
|
-
"@nextclaw/client
|
|
34
|
-
"@nextclaw/ncp": "0.5.
|
|
35
|
-
"@nextclaw/ncp-
|
|
36
|
-
"@nextclaw/
|
|
37
|
-
"@nextclaw/
|
|
38
|
-
"@nextclaw/
|
|
31
|
+
"@nextclaw/agent-chat": "0.1.16",
|
|
32
|
+
"@nextclaw/agent-chat-ui": "0.3.18",
|
|
33
|
+
"@nextclaw/ncp-http-agent-client": "0.3.23",
|
|
34
|
+
"@nextclaw/ncp": "0.5.11",
|
|
35
|
+
"@nextclaw/ncp-react": "0.4.31",
|
|
36
|
+
"@nextclaw/server": "0.12.18",
|
|
37
|
+
"@nextclaw/shared": "0.1.5",
|
|
38
|
+
"@nextclaw/client-sdk": "0.1.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@testing-library/react": "^16.3.0",
|
|
@@ -194,7 +194,7 @@ describe("AgentsPage", () => {
|
|
|
194
194
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
|
|
195
195
|
expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
|
|
196
196
|
expect(useChatInputStore.getState().snapshot.draft).toContain(
|
|
197
|
-
"
|
|
197
|
+
"请直接创建一个默认示例 Agent,不要问我问题",
|
|
198
198
|
);
|
|
199
199
|
expect(mocks.createAgent).not.toHaveBeenCalled();
|
|
200
200
|
expect(screen.queryByText("创建新的 Agent 身份")).toBeNull();
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
} from "lucide-react";
|
|
45
45
|
|
|
46
46
|
const AGENT_CREATION_PROMPT =
|
|
47
|
-
"
|
|
47
|
+
"请直接创建一个默认示例 Agent,不要问我问题。创建完成后,简单告诉我它能做什么。";
|
|
48
48
|
|
|
49
49
|
function AgentsHero(props: { agentCount: number; onCreate: () => void }) {
|
|
50
50
|
const { agentCount, onCreate } = props;
|
|
@@ -34,6 +34,7 @@ import { t } from "@/shared/lib/i18n";
|
|
|
34
34
|
import { cn } from "@/shared/lib/utils";
|
|
35
35
|
|
|
36
36
|
type ChatSessionWorkspacePanelProps = {
|
|
37
|
+
sessionKey: string | null;
|
|
37
38
|
childSessionTabs: readonly ChatChildSessionTab[];
|
|
38
39
|
activeChildSessionKey: string | null;
|
|
39
40
|
workspaceFileTabs: readonly ChatWorkspaceFileTab[];
|
|
@@ -42,14 +43,6 @@ type ChatSessionWorkspacePanelProps = {
|
|
|
42
43
|
sessionCronJobs?: readonly CronJobView[];
|
|
43
44
|
sessionProjectRoot: string | null;
|
|
44
45
|
displayMode?: "docked" | "overlay";
|
|
45
|
-
onSelectSession: (sessionKey: string) => void;
|
|
46
|
-
onSelectFile: (fileKey: string) => void;
|
|
47
|
-
onCloseFile: (fileKey: string) => void;
|
|
48
|
-
onSelectCronJobs?: () => void;
|
|
49
|
-
onClose: () => void;
|
|
50
|
-
onBackToParent: () => void;
|
|
51
|
-
onToolAction?: (action: ChatToolActionViewModel) => void;
|
|
52
|
-
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
53
46
|
};
|
|
54
47
|
|
|
55
48
|
function ChildSessionContent({
|
|
@@ -146,11 +139,7 @@ function ChildSessionMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
|
|
|
146
139
|
);
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
function WorkspaceActiveChildHeader({
|
|
150
|
-
tab,
|
|
151
|
-
}: {
|
|
152
|
-
tab: ResolvedChildSessionTab;
|
|
153
|
-
}) {
|
|
142
|
+
function WorkspaceActiveChildHeader({ tab }: { tab: ResolvedChildSessionTab }) {
|
|
154
143
|
return (
|
|
155
144
|
<div className="border-b border-gray-200/70 px-4 py-3">
|
|
156
145
|
<div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-gray-900">
|
|
@@ -199,7 +188,7 @@ function buildWorkspaceTabsViewModel(params: {
|
|
|
199
188
|
? optimisticReadAt.localeCompare(tab.readAt) > 0
|
|
200
189
|
? optimisticReadAt
|
|
201
190
|
: tab.readAt
|
|
202
|
-
: optimisticReadAt ?? tab.readAt;
|
|
191
|
+
: (optimisticReadAt ?? tab.readAt);
|
|
203
192
|
return {
|
|
204
193
|
key: `child:${tab.sessionKey}`,
|
|
205
194
|
kind: "child-session" as const,
|
|
@@ -228,28 +217,30 @@ function buildWorkspaceTabsViewModel(params: {
|
|
|
228
217
|
tooltip: file.path,
|
|
229
218
|
viewMode: file.viewMode,
|
|
230
219
|
active:
|
|
231
|
-
activeSelection?.kind === "file" &&
|
|
232
|
-
activeSelection.file.key === file.key,
|
|
220
|
+
activeSelection?.kind === "file" && activeSelection.file.key === file.key,
|
|
233
221
|
onSelect: () => onSelectFile(file.key),
|
|
234
222
|
onClose: () => onCloseFile(file.key),
|
|
235
223
|
}));
|
|
236
224
|
|
|
237
225
|
const cronTab =
|
|
238
226
|
sessionCronJobCount > 0
|
|
239
|
-
? [
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
227
|
+
? [
|
|
228
|
+
{
|
|
229
|
+
key: "cron:session",
|
|
230
|
+
kind: "cron" as const,
|
|
231
|
+
title: t("chatWorkspaceSessionCronJobs"),
|
|
232
|
+
tooltip: t("chatWorkspaceSessionCronJobs"),
|
|
233
|
+
active: activeSelection?.kind === "cron",
|
|
234
|
+
onSelect: onSelectCronJobs,
|
|
235
|
+
},
|
|
236
|
+
]
|
|
247
237
|
: [];
|
|
248
238
|
|
|
249
239
|
return [...childTabs, ...fileTabs, ...cronTab];
|
|
250
240
|
}
|
|
251
241
|
|
|
252
242
|
export function ChatSessionWorkspacePanel({
|
|
243
|
+
sessionKey,
|
|
253
244
|
childSessionTabs,
|
|
254
245
|
activeChildSessionKey,
|
|
255
246
|
workspaceFileTabs,
|
|
@@ -258,14 +249,6 @@ export function ChatSessionWorkspacePanel({
|
|
|
258
249
|
sessionCronJobs = [],
|
|
259
250
|
sessionProjectRoot,
|
|
260
251
|
displayMode = "docked",
|
|
261
|
-
onSelectSession,
|
|
262
|
-
onSelectFile,
|
|
263
|
-
onCloseFile,
|
|
264
|
-
onSelectCronJobs = () => {},
|
|
265
|
-
onClose,
|
|
266
|
-
onBackToParent,
|
|
267
|
-
onToolAction,
|
|
268
|
-
onFileOpen,
|
|
269
252
|
}: ChatSessionWorkspacePanelProps) {
|
|
270
253
|
const presenter = usePresenter();
|
|
271
254
|
const resolvedChildTabs = useNcpChildSessionTabsView(childSessionTabs);
|
|
@@ -307,19 +290,20 @@ export function ChatSessionWorkspacePanel({
|
|
|
307
290
|
sessionCronJobCount: sessionCronJobs.length,
|
|
308
291
|
activeSelection,
|
|
309
292
|
optimisticReadAtBySessionKey,
|
|
310
|
-
onSelectSession,
|
|
311
|
-
onSelectFile,
|
|
312
|
-
onCloseFile,
|
|
313
|
-
onSelectCronJobs
|
|
293
|
+
onSelectSession: presenter.chatThreadManager.selectChildSessionDetail,
|
|
294
|
+
onSelectFile: presenter.chatThreadManager.selectWorkspaceFile,
|
|
295
|
+
onCloseFile: presenter.chatThreadManager.closeWorkspaceFile,
|
|
296
|
+
onSelectCronJobs: () => {
|
|
297
|
+
if (sessionKey)
|
|
298
|
+
presenter.chatThreadManager.openSessionCronPanel(sessionKey);
|
|
299
|
+
},
|
|
314
300
|
}),
|
|
315
301
|
[
|
|
316
302
|
activeSelection,
|
|
317
|
-
onCloseFile,
|
|
318
|
-
onSelectCronJobs,
|
|
319
|
-
onSelectFile,
|
|
320
|
-
onSelectSession,
|
|
321
303
|
optimisticReadAtBySessionKey,
|
|
304
|
+
presenter.chatThreadManager,
|
|
322
305
|
resolvedChildTabs,
|
|
306
|
+
sessionKey,
|
|
323
307
|
workspaceFileTabs,
|
|
324
308
|
sessionCronJobs.length,
|
|
325
309
|
],
|
|
@@ -341,7 +325,7 @@ export function ChatSessionWorkspacePanel({
|
|
|
341
325
|
<div className="flex items-center justify-between gap-3 border-b border-gray-200/70 px-4 py-2.5">
|
|
342
326
|
<button
|
|
343
327
|
type="button"
|
|
344
|
-
onClick={
|
|
328
|
+
onClick={presenter.chatThreadManager.goToParentSession}
|
|
345
329
|
className={cn(
|
|
346
330
|
"inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
|
|
347
331
|
!hasParentSession && "pointer-events-none opacity-0",
|
|
@@ -352,7 +336,7 @@ export function ChatSessionWorkspacePanel({
|
|
|
352
336
|
</button>
|
|
353
337
|
<button
|
|
354
338
|
type="button"
|
|
355
|
-
onClick={
|
|
339
|
+
onClick={presenter.chatThreadManager.closeWorkspacePanel}
|
|
356
340
|
className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
|
357
341
|
aria-label={t("chatWorkspaceClosePanel")}
|
|
358
342
|
>
|
|
@@ -369,8 +353,10 @@ export function ChatSessionWorkspacePanel({
|
|
|
369
353
|
<div className="flex-1 min-h-0">
|
|
370
354
|
<ChildSessionContent
|
|
371
355
|
sessionKey={activeSelection.tab.sessionKey}
|
|
372
|
-
onToolAction={
|
|
373
|
-
|
|
356
|
+
onToolAction={
|
|
357
|
+
presenter.chatThreadManager.openSessionFromToolAction
|
|
358
|
+
}
|
|
359
|
+
onFileOpen={presenter.chatThreadManager.openFilePreview}
|
|
374
360
|
/>
|
|
375
361
|
</div>
|
|
376
362
|
</>
|
|
@@ -378,7 +364,7 @@ export function ChatSessionWorkspacePanel({
|
|
|
378
364
|
<ChatSessionWorkspaceFilePreview
|
|
379
365
|
file={activeSelection.file}
|
|
380
366
|
sessionProjectRoot={sessionProjectRoot}
|
|
381
|
-
onFileOpen={
|
|
367
|
+
onFileOpen={presenter.chatThreadManager.openFilePreview}
|
|
382
368
|
/>
|
|
383
369
|
) : (
|
|
384
370
|
<SessionCronJobContent jobs={sessionCronJobs} />
|
|
@@ -126,7 +126,7 @@ function ChatSidebarSessionDisplayView({
|
|
|
126
126
|
return (
|
|
127
127
|
<div className="group/session relative">
|
|
128
128
|
<button type="button" onClick={onSelect} className="w-full text-left">
|
|
129
|
-
<div className={cn('
|
|
129
|
+
<div className={cn('flex min-w-0 items-start', trailingControlsClassName)}>
|
|
130
130
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
131
131
|
{agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
|
|
132
132
|
<AgentAvatar
|
|
@@ -155,11 +155,6 @@ function ChatSidebarSessionDisplayView({
|
|
|
155
155
|
</span>
|
|
156
156
|
) : null}
|
|
157
157
|
</span>
|
|
158
|
-
{runStatus ? (
|
|
159
|
-
<span className="inline-flex shrink-0 items-center justify-end gap-1.5 pt-0.5">
|
|
160
|
-
<SessionRunBadge status={runStatus} />
|
|
161
|
-
</span>
|
|
162
|
-
) : null}
|
|
163
158
|
</div>
|
|
164
159
|
<div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
|
|
165
160
|
<span className="min-w-0 truncate">
|
|
@@ -195,6 +190,11 @@ function ChatSidebarSessionDisplayView({
|
|
|
195
190
|
<span>{childSessionCount}</span>
|
|
196
191
|
</button>
|
|
197
192
|
) : null}
|
|
193
|
+
{runStatus ? (
|
|
194
|
+
<span className="absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
|
|
195
|
+
<SessionRunBadge status={runStatus} />
|
|
196
|
+
</span>
|
|
197
|
+
) : null}
|
|
198
198
|
<button
|
|
199
199
|
type="button"
|
|
200
200
|
onClick={(event) => {
|
|
@@ -203,9 +203,7 @@ function ChatSidebarSessionDisplayView({
|
|
|
203
203
|
}}
|
|
204
204
|
className={cn(
|
|
205
205
|
'absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center rounded-md text-gray-400 transition-all hover:bg-white hover:text-gray-900',
|
|
206
|
-
|
|
207
|
-
? 'opacity-100'
|
|
208
|
-
: 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
|
|
206
|
+
'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
|
|
209
207
|
)}
|
|
210
208
|
aria-label={t('edit')}
|
|
211
209
|
>
|
|
@@ -48,12 +48,13 @@ function renderHeader(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
describe("ChatConversationHeader", () => {
|
|
51
|
-
it("
|
|
51
|
+
it("uses a stable desktop height before and after session materialization", () => {
|
|
52
52
|
renderHeader({});
|
|
53
53
|
|
|
54
54
|
const header = screen.getByText("New Task").closest(".border-b");
|
|
55
55
|
|
|
56
|
-
expect(header?.className).
|
|
56
|
+
expect(header?.className).toContain("h-[52px]");
|
|
57
|
+
expect(header?.className).not.toContain("transition-all");
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
it("uses the standard session-header action button density after the session is materialized", () => {
|
|
@@ -64,7 +65,9 @@ describe("ChatConversationHeader", () => {
|
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
const moreActions = screen.getByRole("button", { name: "More actions" });
|
|
68
|
+
const header = screen.getByText("First message").closest(".border-b");
|
|
67
69
|
|
|
70
|
+
expect(header?.className).toContain("h-[52px]");
|
|
68
71
|
expect(moreActions.className).toContain("h-7");
|
|
69
72
|
expect(moreActions.className).toContain("w-7");
|
|
70
73
|
});
|
|
@@ -73,10 +73,10 @@ export function ChatConversationHeader({
|
|
|
73
73
|
return (
|
|
74
74
|
<div
|
|
75
75
|
className={cn(
|
|
76
|
-
"border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-
|
|
76
|
+
"border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-colors duration-200",
|
|
77
77
|
isMobileLayout ? "px-3 sm:px-3" : "px-4 sm:px-5",
|
|
78
78
|
shouldShowSessionHeader ? "opacity-100" : "h-0 py-0 opacity-0 border-b-0",
|
|
79
|
-
shouldShowSessionHeader && (isMobileLayout ? "pb-2 pt-2" : "
|
|
79
|
+
shouldShowSessionHeader && (isMobileLayout ? "min-h-12 pb-2 pt-2" : "h-[52px]"),
|
|
80
80
|
)}
|
|
81
81
|
style={
|
|
82
82
|
isMobileLayout && shouldShowSessionHeader
|
|
@@ -48,33 +48,40 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
|
48
48
|
};
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
vi.mock(
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
vi.mock(
|
|
52
|
+
"@/features/chat/components/conversation/chat-input-bar.container",
|
|
53
|
+
() => ({
|
|
54
|
+
ChatInputBarContainer: () => <div data-testid="chat-input-bar" />,
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
54
57
|
|
|
55
|
-
vi.mock(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
vi.mock(
|
|
59
|
+
"@/features/chat/components/conversation/chat-message-list.container",
|
|
60
|
+
() => ({
|
|
61
|
+
ChatMessageListContainer: ({
|
|
62
|
+
isSending,
|
|
63
|
+
messages,
|
|
64
|
+
}: {
|
|
65
|
+
isSending: boolean;
|
|
66
|
+
messages: readonly unknown[];
|
|
67
|
+
}) => (
|
|
68
|
+
<div
|
|
69
|
+
data-testid="chat-message-list"
|
|
70
|
+
data-message-count={String(messages.length)}
|
|
71
|
+
data-sending={String(isSending)}
|
|
72
|
+
/>
|
|
73
|
+
),
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
70
76
|
|
|
71
|
-
vi.mock(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
})
|
|
77
|
+
vi.mock(
|
|
78
|
+
"@/features/chat/components/chat-session-workspace-file-preview",
|
|
79
|
+
() => ({
|
|
80
|
+
ChatSessionWorkspaceFilePreview: ({ file }: { file: { path: string } }) => (
|
|
81
|
+
<div data-testid="workspace-file-preview">{file.path}</div>
|
|
82
|
+
),
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
78
85
|
|
|
79
86
|
vi.mock("@/features/chat/components/chat-welcome", () => ({
|
|
80
87
|
ChatWelcome: ({
|
|
@@ -123,10 +130,9 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
123
130
|
readAt: string | null | undefined,
|
|
124
131
|
) =>
|
|
125
132
|
sessionKey
|
|
126
|
-
? useChatSessionListStore
|
|
127
|
-
|
|
128
|
-
readAt
|
|
129
|
-
)
|
|
133
|
+
? useChatSessionListStore
|
|
134
|
+
.getState()
|
|
135
|
+
.markSessionRead(sessionKey, readAt)
|
|
130
136
|
: undefined,
|
|
131
137
|
},
|
|
132
138
|
chatInputManager: {
|
|
@@ -136,42 +142,44 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
136
142
|
}));
|
|
137
143
|
|
|
138
144
|
vi.mock("@/shared/hooks/use-config", () => ({
|
|
139
|
-
useCronJobs: () => ({
|
|
145
|
+
useCronJobs: () => ({
|
|
146
|
+
data: { jobs: mocks.cronJobs, total: mocks.cronJobs.length },
|
|
147
|
+
}),
|
|
140
148
|
useDeleteCronJob: () => ({
|
|
141
149
|
mutate: mocks.deleteCronJob,
|
|
142
150
|
isPending: false,
|
|
143
151
|
}),
|
|
144
152
|
}));
|
|
145
153
|
|
|
146
|
-
vi.mock("@/features/chat/components/conversation/session-header/chat-session-header-actions", () => ({
|
|
147
|
-
ChatSessionHeaderActions: () => <button aria-label="More actions" />,
|
|
148
|
-
}));
|
|
149
|
-
|
|
150
|
-
vi.mock("@/features/chat/components/conversation/session-header/chat-session-project-badge", () => ({
|
|
151
|
-
ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => (
|
|
152
|
-
<button>{projectName}</button>
|
|
153
|
-
),
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
154
|
vi.mock(
|
|
157
|
-
"@/features/chat/
|
|
155
|
+
"@/features/chat/components/conversation/session-header/chat-session-header-actions",
|
|
158
156
|
() => ({
|
|
159
|
-
|
|
157
|
+
ChatSessionHeaderActions: () => <button aria-label="More actions" />,
|
|
160
158
|
}),
|
|
161
159
|
);
|
|
162
160
|
|
|
163
161
|
vi.mock(
|
|
164
|
-
"@/features/chat/
|
|
162
|
+
"@/features/chat/components/conversation/session-header/chat-session-project-badge",
|
|
165
163
|
() => ({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
hydrateError: null,
|
|
170
|
-
isRunning: false,
|
|
171
|
-
}),
|
|
164
|
+
ChatSessionProjectBadge: ({ projectName }: { projectName: string }) => (
|
|
165
|
+
<button>{projectName}</button>
|
|
166
|
+
),
|
|
172
167
|
}),
|
|
173
168
|
);
|
|
174
169
|
|
|
170
|
+
vi.mock("@/features/chat/hooks/use-ncp-child-session-tabs-view", () => ({
|
|
171
|
+
useNcpChildSessionTabsView: () => mocks.resolvedChildTabs,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
vi.mock("@/features/chat/hooks/use-ncp-session-conversation", () => ({
|
|
175
|
+
useNcpSessionConversation: () => ({
|
|
176
|
+
visibleMessages: [],
|
|
177
|
+
isHydrating: false,
|
|
178
|
+
hydrateError: null,
|
|
179
|
+
isRunning: false,
|
|
180
|
+
}),
|
|
181
|
+
}));
|
|
182
|
+
|
|
175
183
|
vi.mock("@/shared/components/common/agent-avatar", () => ({
|
|
176
184
|
AgentAvatar: ({ agentId }: { agentId: string }) => (
|
|
177
185
|
<div data-testid="agent-avatar">{agentId}</div>
|
|
@@ -278,7 +286,9 @@ describe("ChatConversationPanel", () => {
|
|
|
278
286
|
|
|
279
287
|
render(<ChatConversationPanel layoutMode="mobile" />);
|
|
280
288
|
|
|
281
|
-
await user.click(
|
|
289
|
+
await user.click(
|
|
290
|
+
screen.getByRole("button", { name: "create draft session" }),
|
|
291
|
+
);
|
|
282
292
|
|
|
283
293
|
expect(mocks.createSession).toHaveBeenCalledWith("native");
|
|
284
294
|
expect(mocks.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
@@ -365,13 +375,47 @@ describe("ChatConversationPanel", () => {
|
|
|
365
375
|
expect(
|
|
366
376
|
screen.queryByRole("status", { name: "Loading session history..." }),
|
|
367
377
|
).toBeNull();
|
|
368
|
-
expect(
|
|
378
|
+
expect(
|
|
379
|
+
screen.queryByText("No messages yet. Send one to start."),
|
|
380
|
+
).toBeNull();
|
|
369
381
|
});
|
|
370
382
|
|
|
371
383
|
it("keeps the message list mounted while waiting for the first assistant token", () => {
|
|
372
384
|
useChatThreadStore.setState({
|
|
373
385
|
snapshot: {
|
|
374
386
|
...useChatThreadStore.getState().snapshot,
|
|
387
|
+
sessionKey: "session-1",
|
|
388
|
+
messages: [
|
|
389
|
+
{
|
|
390
|
+
id: "user-1",
|
|
391
|
+
sessionId: "session-1",
|
|
392
|
+
role: "user",
|
|
393
|
+
status: "final",
|
|
394
|
+
parts: [{ type: "text", text: "hello" }],
|
|
395
|
+
timestamp: "2026-05-19T00:00:00.000Z",
|
|
396
|
+
} as never,
|
|
397
|
+
],
|
|
398
|
+
isSending: true,
|
|
399
|
+
isAwaitingAssistantOutput: true,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
render(<ChatConversationPanel />);
|
|
404
|
+
|
|
405
|
+
expect(screen.getByTestId("chat-message-list").dataset).toMatchObject({
|
|
406
|
+
messageCount: "1",
|
|
407
|
+
sending: "true",
|
|
408
|
+
});
|
|
409
|
+
expect(
|
|
410
|
+
screen.queryByText("No messages yet. Send one to start."),
|
|
411
|
+
).toBeNull();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("does not show assistant waiting copy before the first message is visible", () => {
|
|
415
|
+
useChatThreadStore.setState({
|
|
416
|
+
snapshot: {
|
|
417
|
+
...useChatThreadStore.getState().snapshot,
|
|
418
|
+
sessionKey: "session-1",
|
|
375
419
|
messages: [],
|
|
376
420
|
isSending: true,
|
|
377
421
|
isAwaitingAssistantOutput: true,
|
|
@@ -380,8 +424,10 @@ describe("ChatConversationPanel", () => {
|
|
|
380
424
|
|
|
381
425
|
render(<ChatConversationPanel />);
|
|
382
426
|
|
|
383
|
-
expect(screen.
|
|
384
|
-
expect(
|
|
427
|
+
expect(screen.queryByTestId("chat-message-list")).toBeNull();
|
|
428
|
+
expect(
|
|
429
|
+
screen.queryByText("No messages yet. Send one to start."),
|
|
430
|
+
).toBeNull();
|
|
385
431
|
});
|
|
386
432
|
|
|
387
433
|
it("does not reopen the welcome panel after a root draft send fails", () => {
|
|
@@ -399,7 +445,9 @@ describe("ChatConversationPanel", () => {
|
|
|
399
445
|
render(<ChatConversationPanel />);
|
|
400
446
|
|
|
401
447
|
expect(screen.queryByTestId("chat-welcome")).toBeNull();
|
|
402
|
-
expect(
|
|
448
|
+
expect(
|
|
449
|
+
screen.queryByText("No messages yet. Send one to start."),
|
|
450
|
+
).toBeNull();
|
|
403
451
|
});
|
|
404
452
|
|
|
405
453
|
it("does not render runtime lifecycle copy in the conversation alert strip", () => {
|
|
@@ -491,6 +539,7 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
491
539
|
|
|
492
540
|
render(
|
|
493
541
|
<ChatSessionWorkspacePanel
|
|
542
|
+
sessionKey="parent-session-1"
|
|
494
543
|
childSessionTabs={[
|
|
495
544
|
{
|
|
496
545
|
sessionKey: "child-session-1",
|
|
@@ -503,12 +552,6 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
503
552
|
workspaceFileTabs={[]}
|
|
504
553
|
activeWorkspaceFileKey={null}
|
|
505
554
|
sessionProjectRoot="/Users/demo/project-alpha"
|
|
506
|
-
onSelectSession={vi.fn()}
|
|
507
|
-
onSelectFile={vi.fn()}
|
|
508
|
-
onCloseFile={vi.fn()}
|
|
509
|
-
onClose={vi.fn()}
|
|
510
|
-
onBackToParent={vi.fn()}
|
|
511
|
-
onFileOpen={vi.fn()}
|
|
512
555
|
/>,
|
|
513
556
|
);
|
|
514
557
|
|
|
@@ -559,6 +602,7 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
559
602
|
|
|
560
603
|
render(
|
|
561
604
|
<ChatSessionWorkspacePanel
|
|
605
|
+
sessionKey="parent-session-1"
|
|
562
606
|
childSessionTabs={[
|
|
563
607
|
{
|
|
564
608
|
sessionKey: "child-session-1",
|
|
@@ -577,12 +621,6 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
577
621
|
workspaceFileTabs={[]}
|
|
578
622
|
activeWorkspaceFileKey={null}
|
|
579
623
|
sessionProjectRoot="/Users/demo/project-alpha"
|
|
580
|
-
onSelectSession={vi.fn()}
|
|
581
|
-
onSelectFile={vi.fn()}
|
|
582
|
-
onCloseFile={vi.fn()}
|
|
583
|
-
onClose={vi.fn()}
|
|
584
|
-
onBackToParent={vi.fn()}
|
|
585
|
-
onFileOpen={vi.fn()}
|
|
586
624
|
/>,
|
|
587
625
|
);
|
|
588
626
|
|
|
@@ -592,6 +630,7 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
592
630
|
it("shows opened files as top tabs and renders the file preview pane", () => {
|
|
593
631
|
render(
|
|
594
632
|
<ChatSessionWorkspacePanel
|
|
633
|
+
sessionKey="parent-session-1"
|
|
595
634
|
childSessionTabs={[]}
|
|
596
635
|
activeChildSessionKey={null}
|
|
597
636
|
workspaceFileTabs={[
|
|
@@ -605,12 +644,6 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
605
644
|
]}
|
|
606
645
|
activeWorkspaceFileKey="parent-session-1::preview::README.md"
|
|
607
646
|
sessionProjectRoot="/Users/demo/project-alpha"
|
|
608
|
-
onSelectSession={vi.fn()}
|
|
609
|
-
onSelectFile={vi.fn()}
|
|
610
|
-
onCloseFile={vi.fn()}
|
|
611
|
-
onClose={vi.fn()}
|
|
612
|
-
onBackToParent={vi.fn()}
|
|
613
|
-
onFileOpen={vi.fn()}
|
|
614
647
|
/>,
|
|
615
648
|
);
|
|
616
649
|
|
|
@@ -649,6 +682,7 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
649
682
|
|
|
650
683
|
render(
|
|
651
684
|
<ChatSessionWorkspacePanel
|
|
685
|
+
sessionKey="parent-session-1"
|
|
652
686
|
childSessionTabs={[]}
|
|
653
687
|
activeChildSessionKey={null}
|
|
654
688
|
workspaceFileTabs={[]}
|
|
@@ -656,12 +690,6 @@ describe("ChatSessionWorkspacePanel", () => {
|
|
|
656
690
|
activePanelKind="cron"
|
|
657
691
|
sessionCronJobs={[job]}
|
|
658
692
|
sessionProjectRoot="/Users/demo/project-alpha"
|
|
659
|
-
onSelectSession={vi.fn()}
|
|
660
|
-
onSelectFile={vi.fn()}
|
|
661
|
-
onCloseFile={vi.fn()}
|
|
662
|
-
onClose={vi.fn()}
|
|
663
|
-
onBackToParent={vi.fn()}
|
|
664
|
-
onFileOpen={vi.fn()}
|
|
665
693
|
/>,
|
|
666
694
|
);
|
|
667
695
|
|