@nextclaw/ui 0.15.5 → 0.15.6
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 +24 -0
- package/dist/assets/api-Dyb5rgh0.js +13 -0
- package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
- package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
- package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
- package/dist/assets/index-BVn702m4.css +1 -0
- package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
- package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
- package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
- package/dist/assets/plus-TmFdwO3P.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
- package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
- package/dist/assets/remote-B9F2uCk9.js +1 -0
- package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
- package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
- package/dist/assets/security-config-Cr2ERKLu.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
- package/dist/assets/use-config-CwDEAl6f.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
- package/dist/assets/x-DdEDL6ju.js +1 -0
- package/dist/index.html +29 -29
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
- 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 +52 -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 +37 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -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 +8 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -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 +50 -4
- 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 +66 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -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 +81 -127
- 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/pages/__tests__/ncp-chat-page.test.ts +23 -0
- 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 +35 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -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/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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import { Check, ChevronDown, ChevronRight, Folder } from "lucide-react";
|
|
3
|
+
import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
|
|
4
|
+
import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
|
|
5
|
+
import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
|
|
6
|
+
import type {
|
|
7
|
+
ChatSidebarDateGroup,
|
|
8
|
+
ChatSidebarProjectGroup,
|
|
9
|
+
} from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
|
|
10
|
+
import {
|
|
11
|
+
formatSessionListTime,
|
|
12
|
+
sessionActivityPreviewText,
|
|
13
|
+
sessionDisplayName,
|
|
14
|
+
} from "@/features/chat/features/session/utils/chat-session-display.utils";
|
|
15
|
+
import { resolveSessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
|
|
16
|
+
import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
17
|
+
import { shouldShowUnreadSessionIndicator } from "@/features/chat/stores/chat-session-list.store";
|
|
18
|
+
import { t } from "@/shared/lib/i18n";
|
|
19
|
+
import { cn } from "@/shared/lib/utils";
|
|
20
|
+
|
|
21
|
+
type SessionSwitchItemsProps = {
|
|
22
|
+
items: NcpSessionListItemView[];
|
|
23
|
+
selectedSessionKey: string | null;
|
|
24
|
+
optimisticReadAtBySessionKey: Record<string, string>;
|
|
25
|
+
sessionTypeOptions: ChatSessionTypeOption[];
|
|
26
|
+
onSelect: (sessionKey: string) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function ChatSessionSwitchItem({
|
|
31
|
+
item,
|
|
32
|
+
selectedSessionKey,
|
|
33
|
+
optimisticReadAtBySessionKey,
|
|
34
|
+
sessionTypeOptions,
|
|
35
|
+
onSelect,
|
|
36
|
+
}: Omit<SessionSwitchItemsProps, "items" | "className"> & {
|
|
37
|
+
item: NcpSessionListItemView;
|
|
38
|
+
}) {
|
|
39
|
+
const { session, runStatus } = item;
|
|
40
|
+
const active = selectedSessionKey === session.key;
|
|
41
|
+
const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
|
|
42
|
+
const effectiveReadAt =
|
|
43
|
+
optimisticReadAt && session.readAt
|
|
44
|
+
? optimisticReadAt.localeCompare(session.readAt) > 0
|
|
45
|
+
? optimisticReadAt
|
|
46
|
+
: session.readAt
|
|
47
|
+
: (optimisticReadAt ?? session.readAt);
|
|
48
|
+
const shouldShowUnread = shouldShowUnreadSessionIndicator({
|
|
49
|
+
active,
|
|
50
|
+
lastMessageAt: session.lastMessageAt,
|
|
51
|
+
readAt: effectiveReadAt,
|
|
52
|
+
runStatus,
|
|
53
|
+
});
|
|
54
|
+
const previewText =
|
|
55
|
+
sessionActivityPreviewText(session) ??
|
|
56
|
+
session.projectName ??
|
|
57
|
+
session.projectRoot ??
|
|
58
|
+
`${session.messageCount}`;
|
|
59
|
+
const trailingText = formatSessionListTime(
|
|
60
|
+
session.lastMessageAt ?? session.createdAt,
|
|
61
|
+
);
|
|
62
|
+
const sessionContext = resolveSessionContextView(session, sessionTypeOptions);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
aria-current={active ? "page" : undefined}
|
|
68
|
+
className={cn(
|
|
69
|
+
"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",
|
|
70
|
+
active
|
|
71
|
+
? "bg-accent text-accent-foreground"
|
|
72
|
+
: "text-foreground hover:bg-accent/65 hover:text-accent-foreground",
|
|
73
|
+
)}
|
|
74
|
+
onClick={() => onSelect(session.key)}
|
|
75
|
+
>
|
|
76
|
+
<span className="relative mt-1 flex h-2.5 w-2.5 shrink-0 items-center justify-center">
|
|
77
|
+
{shouldShowUnread ? (
|
|
78
|
+
<span
|
|
79
|
+
aria-label={t("chatSidebarUnread")}
|
|
80
|
+
className="h-1.5 w-1.5 rounded-full bg-primary"
|
|
81
|
+
/>
|
|
82
|
+
) : active ? (
|
|
83
|
+
<Check className="h-3 w-3 text-primary" />
|
|
84
|
+
) : (
|
|
85
|
+
<span className="h-1 w-1 rounded-full bg-muted-foreground/45" />
|
|
86
|
+
)}
|
|
87
|
+
</span>
|
|
88
|
+
<span className="min-w-0 flex-1">
|
|
89
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
90
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium">
|
|
91
|
+
{sessionDisplayName(session)}
|
|
92
|
+
</span>
|
|
93
|
+
{sessionContext.label ? (
|
|
94
|
+
<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">
|
|
95
|
+
{sessionContext.label}
|
|
96
|
+
</span>
|
|
97
|
+
) : null}
|
|
98
|
+
{sessionContext.icon ? (
|
|
99
|
+
<span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center text-muted-foreground">
|
|
100
|
+
<SessionContextIconNode icon={sessionContext.icon} />
|
|
101
|
+
</span>
|
|
102
|
+
) : null}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
|
|
105
|
+
<span className="min-w-0 truncate">{previewText}</span>
|
|
106
|
+
<span className="shrink-0">{trailingText}</span>
|
|
107
|
+
</span>
|
|
108
|
+
</span>
|
|
109
|
+
{runStatus ? (
|
|
110
|
+
<span className="mt-0.5 shrink-0">
|
|
111
|
+
<SessionRunBadge status={runStatus} />
|
|
112
|
+
</span>
|
|
113
|
+
) : null}
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ChatSessionSwitchItems({
|
|
119
|
+
items,
|
|
120
|
+
selectedSessionKey,
|
|
121
|
+
optimisticReadAtBySessionKey,
|
|
122
|
+
sessionTypeOptions,
|
|
123
|
+
onSelect,
|
|
124
|
+
className,
|
|
125
|
+
}: SessionSwitchItemsProps) {
|
|
126
|
+
return (
|
|
127
|
+
<div className={cn("space-y-1", className)}>
|
|
128
|
+
{items.map((item) => (
|
|
129
|
+
<ChatSessionSwitchItem
|
|
130
|
+
key={item.session.key}
|
|
131
|
+
item={item}
|
|
132
|
+
selectedSessionKey={selectedSessionKey}
|
|
133
|
+
optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
|
|
134
|
+
sessionTypeOptions={sessionTypeOptions}
|
|
135
|
+
onSelect={onSelect}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ChatSessionSwitchDateGroup({
|
|
143
|
+
group,
|
|
144
|
+
...itemsProps
|
|
145
|
+
}: Omit<SessionSwitchItemsProps, "items"> & {
|
|
146
|
+
group: ChatSidebarDateGroup;
|
|
147
|
+
}) {
|
|
148
|
+
return (
|
|
149
|
+
<section aria-label={group.label}>
|
|
150
|
+
<div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
|
151
|
+
{group.label}
|
|
152
|
+
</div>
|
|
153
|
+
<ChatSessionSwitchItems items={group.items} {...itemsProps} />
|
|
154
|
+
</section>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ChatSessionSwitchProjectGroup({
|
|
159
|
+
group,
|
|
160
|
+
isCollapsed,
|
|
161
|
+
onToggleCollapsed,
|
|
162
|
+
...itemsProps
|
|
163
|
+
}: Omit<SessionSwitchItemsProps, "items"> & {
|
|
164
|
+
group: ChatSidebarProjectGroup;
|
|
165
|
+
isCollapsed: boolean;
|
|
166
|
+
onToggleCollapsed: (projectRoot: string) => void;
|
|
167
|
+
}) {
|
|
168
|
+
const contentId = useId();
|
|
169
|
+
const actionLabel = `${t(
|
|
170
|
+
isCollapsed ? "chatSidebarExpandProject" : "chatSidebarCollapseProject",
|
|
171
|
+
)} · ${group.projectName}`;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<section aria-label={group.projectName}>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
aria-controls={contentId}
|
|
178
|
+
aria-expanded={!isCollapsed}
|
|
179
|
+
aria-label={actionLabel}
|
|
180
|
+
className="group flex h-10 w-full min-w-0 items-center gap-1.5 rounded-lg px-2 text-left text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
|
|
181
|
+
onClick={() => onToggleCollapsed(group.projectRoot)}
|
|
182
|
+
>
|
|
183
|
+
<Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
|
184
|
+
<span
|
|
185
|
+
className="min-w-0 flex-1 truncate text-[11px] font-medium uppercase tracking-wider"
|
|
186
|
+
title={group.projectRoot}
|
|
187
|
+
>
|
|
188
|
+
{group.projectName}
|
|
189
|
+
</span>
|
|
190
|
+
<span className="shrink-0 text-[10px] text-muted-foreground/70">
|
|
191
|
+
{group.items.length}
|
|
192
|
+
</span>
|
|
193
|
+
{isCollapsed ? (
|
|
194
|
+
<ChevronRight className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
|
195
|
+
) : (
|
|
196
|
+
<ChevronDown className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
|
197
|
+
)}
|
|
198
|
+
</button>
|
|
199
|
+
{isCollapsed ? null : (
|
|
200
|
+
<ChatSessionSwitchItems
|
|
201
|
+
items={group.items}
|
|
202
|
+
className="mt-0.5"
|
|
203
|
+
{...itemsProps}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
</section>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function ChatSessionSwitcherGroups({
|
|
211
|
+
collapsedProjectRoots,
|
|
212
|
+
dateGroups,
|
|
213
|
+
isProjectFirstView,
|
|
214
|
+
onToggleProjectCollapsed,
|
|
215
|
+
projectGroups,
|
|
216
|
+
...itemsProps
|
|
217
|
+
}: Omit<SessionSwitchItemsProps, "items"> & {
|
|
218
|
+
collapsedProjectRoots: string[];
|
|
219
|
+
dateGroups: ChatSidebarDateGroup[];
|
|
220
|
+
isProjectFirstView: boolean;
|
|
221
|
+
onToggleProjectCollapsed: (projectRoot: string) => void;
|
|
222
|
+
projectGroups: ChatSidebarProjectGroup[];
|
|
223
|
+
}) {
|
|
224
|
+
return (
|
|
225
|
+
<div className="space-y-3">
|
|
226
|
+
{isProjectFirstView
|
|
227
|
+
? projectGroups.map((group) => (
|
|
228
|
+
<ChatSessionSwitchProjectGroup
|
|
229
|
+
key={group.projectRoot}
|
|
230
|
+
group={group}
|
|
231
|
+
isCollapsed={collapsedProjectRoots.includes(group.projectRoot)}
|
|
232
|
+
onToggleCollapsed={onToggleProjectCollapsed}
|
|
233
|
+
{...itemsProps}
|
|
234
|
+
/>
|
|
235
|
+
))
|
|
236
|
+
: dateGroups.map((group) => (
|
|
237
|
+
<ChatSessionSwitchDateGroup
|
|
238
|
+
key={group.label}
|
|
239
|
+
group={group}
|
|
240
|
+
{...itemsProps}
|
|
241
|
+
/>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
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
|
)}
|
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
|
});
|