@nextclaw/ui 0.14.1-beta.0 → 0.14.1-beta.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 +18 -0
- package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
- package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
- package/dist/assets/chat-page-D3QaEqjs.js +105 -0
- package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
- package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
- package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
- package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
- package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
- package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
- package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
- package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
- package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
- package/dist/assets/{external-link-D2cBOCqn.js → external-link-CcgtBK26.js} +1 -1
- package/dist/assets/index-ByJCbQqr.css +1 -0
- package/dist/assets/index-DjXqiloY.js +103 -0
- package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
- package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
- package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
- package/dist/assets/plus-1Rxl5FlV.js +1 -0
- package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
- package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
- package/dist/assets/{react-DYQXXGbH.js → react-CTqdhFMk.js} +1 -1
- package/dist/assets/remote-fQ7N_r8u.js +1 -0
- package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
- package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
- package/dist/assets/{save-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
- package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
- package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
- package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
- package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
- package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-C5aOtjxu.js} +1 -1
- package/dist/assets/use-config-D8TESeo6.js +1 -0
- package/dist/assets/x-1qETdQ70.js +1 -0
- package/dist/index.html +14 -14
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
- package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
- package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
- package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
- package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
- package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
- package/src/features/chat/managers/chat-input.manager.ts +24 -2
- package/src/features/chat/stores/chat-input.store.ts +2 -0
- package/src/features/chat/types/chat-input-bar.types.ts +5 -0
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
- package/src/features/settings/components/config/provider-form.tsx +284 -387
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
- package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
- package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
- package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
- package/src/features/settings/pages/secrets-config-page.tsx +2 -1
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
- package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
- package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
- package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
- package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
- package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
- package/src/features/system-status/components/runtime-security-card.tsx +79 -25
- package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
- package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
- package/src/shared/components/common/searchable-model-input.tsx +102 -87
- package/src/shared/components/ui/README.md +7 -0
- package/src/shared/components/ui/popover.tsx +19 -3
- package/src/shared/components/ui/select.tsx +14 -4
- package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
- package/src/shared/lib/api/index.ts +2 -0
- package/src/shared/lib/api/preferences/preference.types.ts +22 -0
- package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/dist/assets/chat-page-BXngLJY6.js +0 -105
- package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
- package/dist/assets/index-DTb_XVIH.js +0 -103
- package/dist/assets/index-ayiPYhf1.css +0 -1
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
- package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
- package/dist/assets/plus-CYE1onU2.js +0 -1
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
- package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
- package/dist/assets/remote-BlJDbXOI.js +0 -1
- package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
- package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
- package/dist/assets/use-config-DgKM9VIU.js +0 -1
- package/dist/assets/x-Cm3JjYut.js +0 -1
- package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
- package/src/features/chat/components/chat-welcome.tsx +0 -102
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Bot } from 'lucide-react';
|
|
4
|
+
import { ChatSessionProjectDialog } from '@/features/chat/features/session/components/session-header/chat-session-project-dialog';
|
|
5
|
+
import { ChatWelcomeAgentPicker } from '@/features/chat/features/welcome/components/chat-welcome-agent-picker';
|
|
6
|
+
import { ChatWelcomeCapabilityGrid } from '@/features/chat/features/welcome/components/chat-welcome-capability-grid';
|
|
7
|
+
import { ChatWelcomeProjectPicker } from '@/features/chat/features/welcome/components/chat-welcome-project-picker';
|
|
8
|
+
import { ChatWelcomeSessionTypePicker } from '@/features/chat/features/welcome/components/chat-welcome-session-type-picker';
|
|
9
|
+
import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
|
|
10
|
+
import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
|
|
11
|
+
import type { AgentProfileView } from '@/shared/lib/api';
|
|
12
|
+
import { t } from '@/shared/lib/i18n';
|
|
13
|
+
|
|
14
|
+
type SessionTypeOption = ChatInputSnapshot['sessionTypeOptions'][number];
|
|
15
|
+
|
|
16
|
+
type ChatWelcomeProps = {
|
|
17
|
+
agents: AgentProfileView[];
|
|
18
|
+
inputSlot?: ReactNode;
|
|
19
|
+
defaultProjectRoot?: string | null;
|
|
20
|
+
projectOptions: readonly ChatWelcomeProjectOption[];
|
|
21
|
+
selectedAgentId: string;
|
|
22
|
+
selectedProjectRoot?: string | null;
|
|
23
|
+
selectedSessionType: string;
|
|
24
|
+
sessionTypeOptions: readonly SessionTypeOption[];
|
|
25
|
+
onSelectAgent: (agentId: string) => void;
|
|
26
|
+
onSelectPrompt: (prompt: string) => void;
|
|
27
|
+
onSelectProjectRoot?: (projectRoot: string) => Promise<void> | void;
|
|
28
|
+
onSelectSessionType: (sessionType: string) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function ChatWelcome({
|
|
32
|
+
agents,
|
|
33
|
+
defaultProjectRoot,
|
|
34
|
+
inputSlot,
|
|
35
|
+
projectOptions,
|
|
36
|
+
selectedAgentId,
|
|
37
|
+
selectedProjectRoot,
|
|
38
|
+
selectedSessionType,
|
|
39
|
+
sessionTypeOptions,
|
|
40
|
+
onSelectAgent,
|
|
41
|
+
onSelectPrompt,
|
|
42
|
+
onSelectProjectRoot,
|
|
43
|
+
onSelectSessionType,
|
|
44
|
+
}: ChatWelcomeProps) {
|
|
45
|
+
const selectedAgent = agents.find((agent) => agent.id === selectedAgentId) ?? null;
|
|
46
|
+
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false);
|
|
47
|
+
const [isProjectSaving, setIsProjectSaving] = useState(false);
|
|
48
|
+
const resolvedProjectRoot = selectedProjectRoot ?? defaultProjectRoot ?? null;
|
|
49
|
+
|
|
50
|
+
const saveProjectRoot = async (projectRoot: string) => {
|
|
51
|
+
if (!onSelectProjectRoot) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setIsProjectSaving(true);
|
|
55
|
+
try {
|
|
56
|
+
await onSelectProjectRoot(projectRoot);
|
|
57
|
+
setIsProjectDialogOpen(false);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsProjectSaving(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const selectableProjectRoot = Boolean(onSelectProjectRoot);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex min-h-full items-center justify-center px-4 py-8 sm:p-8">
|
|
66
|
+
<div className="min-w-0 w-full max-w-[min(760px,100%)]">
|
|
67
|
+
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10">
|
|
68
|
+
<Bot className="h-8 w-8 text-primary" />
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="text-center">
|
|
72
|
+
<h2 className="text-2xl font-semibold text-gray-950 sm:text-3xl">
|
|
73
|
+
{t('chatWelcomeTitle')}
|
|
74
|
+
</h2>
|
|
75
|
+
<p className="mt-3 text-sm font-medium text-gray-500 sm:text-base">
|
|
76
|
+
{t('chatWelcomeSubtitle')}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{inputSlot ? <div className="mt-8">{inputSlot}</div> : null}
|
|
81
|
+
|
|
82
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 px-1 text-sm text-gray-500">
|
|
83
|
+
{onSelectProjectRoot ? (
|
|
84
|
+
<ChatWelcomeProjectPicker
|
|
85
|
+
defaultProjectRoot={defaultProjectRoot}
|
|
86
|
+
isSaving={isProjectSaving}
|
|
87
|
+
projectOptions={projectOptions}
|
|
88
|
+
projectRoot={resolvedProjectRoot}
|
|
89
|
+
selectable={selectableProjectRoot}
|
|
90
|
+
onOpenProjectDialog={() => setIsProjectDialogOpen(true)}
|
|
91
|
+
onSelectProjectRoot={saveProjectRoot}
|
|
92
|
+
/>
|
|
93
|
+
) : null}
|
|
94
|
+
<ChatWelcomeAgentPicker
|
|
95
|
+
agents={agents}
|
|
96
|
+
selectedAgent={selectedAgent}
|
|
97
|
+
selectedAgentId={selectedAgentId}
|
|
98
|
+
onSelectAgent={onSelectAgent}
|
|
99
|
+
/>
|
|
100
|
+
<ChatWelcomeSessionTypePicker
|
|
101
|
+
options={sessionTypeOptions}
|
|
102
|
+
selectedSessionType={selectedSessionType}
|
|
103
|
+
onSelectSessionType={onSelectSessionType}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<ChatWelcomeCapabilityGrid onSelectPrompt={onSelectPrompt} />
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{onSelectProjectRoot ? (
|
|
111
|
+
<ChatSessionProjectDialog
|
|
112
|
+
open={isProjectDialogOpen}
|
|
113
|
+
currentProjectRoot={resolvedProjectRoot}
|
|
114
|
+
isSaving={isProjectSaving}
|
|
115
|
+
onOpenChange={setIsProjectDialogOpen}
|
|
116
|
+
onSave={saveProjectRoot}
|
|
117
|
+
/>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildChatWelcomeProjectOptions } from "@/features/chat/features/welcome/utils/chat-welcome-project-options.utils";
|
|
3
|
+
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
4
|
+
|
|
5
|
+
function createSummary(
|
|
6
|
+
overrides: Partial<NcpSessionSummaryView> & { sessionId: string },
|
|
7
|
+
): NcpSessionSummaryView {
|
|
8
|
+
return {
|
|
9
|
+
sessionId: overrides.sessionId,
|
|
10
|
+
agentId: "main",
|
|
11
|
+
createdAt: overrides.createdAt ?? "2026-06-10T10:00:00.000Z",
|
|
12
|
+
updatedAt: overrides.updatedAt ?? "2026-06-10T10:00:00.000Z",
|
|
13
|
+
lastMessageAt: overrides.lastMessageAt,
|
|
14
|
+
messageCount: 1,
|
|
15
|
+
metadata: overrides.metadata ?? {},
|
|
16
|
+
status: "idle",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("buildChatWelcomeProjectOptions", () => {
|
|
21
|
+
it("builds recent project options from session summaries", () => {
|
|
22
|
+
const options = buildChatWelcomeProjectOptions({
|
|
23
|
+
defaultProjectRoot: "/Users/demo/.nextclaw/workspace",
|
|
24
|
+
sessionSummaries: [
|
|
25
|
+
createSummary({
|
|
26
|
+
sessionId: "session-1",
|
|
27
|
+
lastMessageAt: "2026-06-10T10:00:00.000Z",
|
|
28
|
+
metadata: { project_root: "/tmp/project-alpha" },
|
|
29
|
+
}),
|
|
30
|
+
createSummary({
|
|
31
|
+
sessionId: "session-2",
|
|
32
|
+
lastMessageAt: "2026-06-11T10:00:00.000Z",
|
|
33
|
+
metadata: { project_root: "/tmp/project-alpha" },
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(options).toEqual([
|
|
39
|
+
{
|
|
40
|
+
projectRoot: "/Users/demo/.nextclaw/workspace",
|
|
41
|
+
projectName: "workspace",
|
|
42
|
+
sessionCount: 0,
|
|
43
|
+
isDefault: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
projectRoot: "/tmp/project-alpha",
|
|
47
|
+
projectName: "project-alpha",
|
|
48
|
+
sessionCount: 2,
|
|
49
|
+
isDefault: false,
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveChatWelcomeAgents,
|
|
4
|
+
resolveChatWelcomeSelectedAgent,
|
|
5
|
+
resolveChatWelcomeSelectedSessionType,
|
|
6
|
+
} from "@/features/chat/features/welcome/utils/chat-welcome-selection.utils";
|
|
7
|
+
|
|
8
|
+
const agents = [
|
|
9
|
+
{ id: "main", displayName: "Main", runtime: "native" },
|
|
10
|
+
{ id: "engineer", displayName: "Engineer", runtime: "codex" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe("chat welcome selection utils", () => {
|
|
14
|
+
it("falls back to the selected agent when agent loading is empty", () => {
|
|
15
|
+
expect(
|
|
16
|
+
resolveChatWelcomeAgents({
|
|
17
|
+
agents: [],
|
|
18
|
+
fallbackAgentId: "main",
|
|
19
|
+
}),
|
|
20
|
+
).toEqual([{ id: "main" }]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("resolves the selected welcome agent", () => {
|
|
24
|
+
expect(
|
|
25
|
+
resolveChatWelcomeSelectedAgent({
|
|
26
|
+
agents,
|
|
27
|
+
agentId: "engineer",
|
|
28
|
+
})?.runtime,
|
|
29
|
+
).toBe("codex");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("resolves the session type from the selected agent runtime", () => {
|
|
33
|
+
expect(
|
|
34
|
+
resolveChatWelcomeSelectedSessionType({
|
|
35
|
+
agents,
|
|
36
|
+
agentId: "engineer",
|
|
37
|
+
defaultSessionType: "native",
|
|
38
|
+
pendingSessionType: null,
|
|
39
|
+
selectedSessionType: null,
|
|
40
|
+
sessionTypeOptions: [
|
|
41
|
+
{ value: "native" },
|
|
42
|
+
{ value: "codex" },
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
).toBe("codex");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("prefers an explicit session type over the selected agent runtime", () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveChatWelcomeSelectedSessionType({
|
|
51
|
+
agents,
|
|
52
|
+
agentId: "engineer",
|
|
53
|
+
defaultSessionType: "native",
|
|
54
|
+
pendingSessionType: "native",
|
|
55
|
+
selectedSessionType: "native",
|
|
56
|
+
sessionTypeOptions: [
|
|
57
|
+
{ value: "native" },
|
|
58
|
+
{ value: "codex" },
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
).toBe("native");
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldShowChatWelcome } from "@/features/chat/features/welcome/utils/chat-welcome-visibility.utils";
|
|
3
|
+
|
|
4
|
+
function createVisibilitySnapshot(
|
|
5
|
+
overrides: Partial<Parameters<typeof shouldShowChatWelcome>[0]> = {},
|
|
6
|
+
): Parameters<typeof shouldShowChatWelcome>[0] {
|
|
7
|
+
return {
|
|
8
|
+
canDeleteSession: false,
|
|
9
|
+
hasSubmittedDraftMessage: false,
|
|
10
|
+
isSending: false,
|
|
11
|
+
messages: [],
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("shouldShowChatWelcome", () => {
|
|
17
|
+
it("shows the welcome entry for an untouched draft", () => {
|
|
18
|
+
expect(shouldShowChatWelcome(createVisibilitySnapshot())).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps the welcome entry hidden after a draft send attempt", () => {
|
|
22
|
+
expect(
|
|
23
|
+
shouldShowChatWelcome(
|
|
24
|
+
createVisibilitySnapshot({ hasSubmittedDraftMessage: true }),
|
|
25
|
+
),
|
|
26
|
+
).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps the welcome entry hidden for an existing session", () => {
|
|
30
|
+
expect(
|
|
31
|
+
shouldShowChatWelcome(createVisibilitySnapshot({ canDeleteSession: true })),
|
|
32
|
+
).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
2
|
+
import { adaptNcpSessionSummaries } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
3
|
+
import { getSessionProjectName } from "@/shared/lib/session-project";
|
|
4
|
+
|
|
5
|
+
export type ChatWelcomeProjectOption = {
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
projectName: string;
|
|
8
|
+
sessionCount: number;
|
|
9
|
+
isDefault: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function readSessionActivityAt(session: {
|
|
13
|
+
createdAt: string;
|
|
14
|
+
lastMessageAt?: string;
|
|
15
|
+
}): number {
|
|
16
|
+
return new Date(session.lastMessageAt ?? session.createdAt).getTime();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildChatWelcomeProjectOptions(params: {
|
|
20
|
+
defaultProjectRoot: string | null;
|
|
21
|
+
sessionSummaries: readonly NcpSessionSummaryView[];
|
|
22
|
+
}): ChatWelcomeProjectOption[] {
|
|
23
|
+
const { defaultProjectRoot, sessionSummaries } = params;
|
|
24
|
+
const groups = new Map<
|
|
25
|
+
string,
|
|
26
|
+
ChatWelcomeProjectOption & { latestUpdatedAt: number }
|
|
27
|
+
>();
|
|
28
|
+
|
|
29
|
+
for (const session of adaptNcpSessionSummaries([...sessionSummaries])) {
|
|
30
|
+
const projectRoot = session.projectRoot?.trim();
|
|
31
|
+
if (!projectRoot) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const existing = groups.get(projectRoot);
|
|
35
|
+
const latestUpdatedAt = readSessionActivityAt(session);
|
|
36
|
+
if (existing) {
|
|
37
|
+
existing.sessionCount += 1;
|
|
38
|
+
existing.latestUpdatedAt = Math.max(existing.latestUpdatedAt, latestUpdatedAt);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
groups.set(projectRoot, {
|
|
42
|
+
projectRoot,
|
|
43
|
+
projectName:
|
|
44
|
+
session.projectName?.trim() ||
|
|
45
|
+
getSessionProjectName(projectRoot) ||
|
|
46
|
+
projectRoot,
|
|
47
|
+
sessionCount: 1,
|
|
48
|
+
isDefault: projectRoot === defaultProjectRoot,
|
|
49
|
+
latestUpdatedAt,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (defaultProjectRoot && !groups.has(defaultProjectRoot)) {
|
|
54
|
+
groups.set(defaultProjectRoot, {
|
|
55
|
+
projectRoot: defaultProjectRoot,
|
|
56
|
+
projectName: getSessionProjectName(defaultProjectRoot) ?? defaultProjectRoot,
|
|
57
|
+
sessionCount: 0,
|
|
58
|
+
isDefault: true,
|
|
59
|
+
latestUpdatedAt: Number.POSITIVE_INFINITY,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return [...groups.values()]
|
|
64
|
+
.sort((left, right) => {
|
|
65
|
+
if (left.isDefault !== right.isDefault) {
|
|
66
|
+
return left.isDefault ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
return right.latestUpdatedAt - left.latestUpdatedAt;
|
|
69
|
+
})
|
|
70
|
+
.map(({ latestUpdatedAt: _latestUpdatedAt, ...option }) => option);
|
|
71
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeSessionType,
|
|
3
|
+
resolveAgentRuntimeSessionType,
|
|
4
|
+
} from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
5
|
+
import type { AgentProfileView } from "@/shared/lib/api";
|
|
6
|
+
|
|
7
|
+
type SessionTypeOptionLike = {
|
|
8
|
+
value: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function resolveChatWelcomeAgents(params: {
|
|
12
|
+
agents: AgentProfileView[] | null | undefined;
|
|
13
|
+
fallbackAgentId: string;
|
|
14
|
+
}): AgentProfileView[] {
|
|
15
|
+
const { agents, fallbackAgentId } = params;
|
|
16
|
+
return (agents?.length ?? 0) > 0 ? (agents ?? []) : [{ id: fallbackAgentId }];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveChatWelcomeSelectedAgent(params: {
|
|
20
|
+
agents: AgentProfileView[];
|
|
21
|
+
agentId: string;
|
|
22
|
+
}): AgentProfileView | null {
|
|
23
|
+
return params.agents.find((agent) => agent.id === params.agentId) ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveAvailableSessionType(
|
|
27
|
+
value: string | null | undefined,
|
|
28
|
+
options: readonly SessionTypeOptionLike[],
|
|
29
|
+
): string | null {
|
|
30
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const normalized = normalizeSessionType(value);
|
|
34
|
+
return options.some((option) => option.value === normalized) ? normalized : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveChatWelcomeSelectedSessionType(params: {
|
|
38
|
+
agents?: AgentProfileView[];
|
|
39
|
+
agentId?: string;
|
|
40
|
+
defaultSessionType: string;
|
|
41
|
+
pendingSessionType: string | null | undefined;
|
|
42
|
+
selectedSessionType: string | null | undefined;
|
|
43
|
+
sessionTypeOptions: readonly SessionTypeOptionLike[];
|
|
44
|
+
}): string {
|
|
45
|
+
const {
|
|
46
|
+
agentId,
|
|
47
|
+
agents,
|
|
48
|
+
defaultSessionType,
|
|
49
|
+
pendingSessionType,
|
|
50
|
+
selectedSessionType,
|
|
51
|
+
sessionTypeOptions,
|
|
52
|
+
} = params;
|
|
53
|
+
const explicitSessionType =
|
|
54
|
+
resolveAvailableSessionType(selectedSessionType, sessionTypeOptions) ??
|
|
55
|
+
resolveAvailableSessionType(pendingSessionType, sessionTypeOptions);
|
|
56
|
+
if (explicitSessionType) {
|
|
57
|
+
return explicitSessionType;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const agentSessionType =
|
|
61
|
+
agents && agentId
|
|
62
|
+
? resolveAvailableSessionType(
|
|
63
|
+
resolveAgentRuntimeSessionType(
|
|
64
|
+
resolveChatWelcomeSelectedAgent({
|
|
65
|
+
agents,
|
|
66
|
+
agentId,
|
|
67
|
+
}),
|
|
68
|
+
defaultSessionType,
|
|
69
|
+
),
|
|
70
|
+
sessionTypeOptions,
|
|
71
|
+
)
|
|
72
|
+
: null;
|
|
73
|
+
return (
|
|
74
|
+
agentSessionType ??
|
|
75
|
+
resolveAvailableSessionType(defaultSessionType, sessionTypeOptions) ??
|
|
76
|
+
sessionTypeOptions[0]?.value ??
|
|
77
|
+
normalizeSessionType(defaultSessionType)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChatThreadSnapshot } from "@/features/chat/stores/chat-thread.store";
|
|
2
|
+
|
|
3
|
+
type ChatWelcomeVisibilitySnapshot = Pick<
|
|
4
|
+
ChatThreadSnapshot,
|
|
5
|
+
"canDeleteSession" | "hasSubmittedDraftMessage" | "isSending" | "messages"
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function shouldShowChatWelcome(
|
|
9
|
+
snapshot: ChatWelcomeVisibilitySnapshot,
|
|
10
|
+
): boolean {
|
|
11
|
+
return (
|
|
12
|
+
!snapshot.canDeleteSession &&
|
|
13
|
+
!snapshot.hasSubmittedDraftMessage &&
|
|
14
|
+
snapshot.messages.length === 0 &&
|
|
15
|
+
!snapshot.isSending
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -138,6 +138,46 @@ describe('ChatInputManager', () => {
|
|
|
138
138
|
);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
it('uses the default workspace as the project root for a blank draft send', async () => {
|
|
142
|
+
useChatInputStore.setState({
|
|
143
|
+
snapshot: {
|
|
144
|
+
...useChatInputStore.getState().snapshot,
|
|
145
|
+
defaultProjectRoot: '/Users/demo/.nextclaw/workspace',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
useChatThreadStore.setState({
|
|
149
|
+
snapshot: {
|
|
150
|
+
...useChatThreadStore.getState().snapshot,
|
|
151
|
+
sessionKey: null,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
useChatSessionListStore.setState({
|
|
155
|
+
snapshot: {
|
|
156
|
+
...useChatSessionListStore.getState().snapshot,
|
|
157
|
+
selectedSessionKey: null,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const chatRunManager = {
|
|
161
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
stopCurrentRun: vi.fn().mockResolvedValue(undefined),
|
|
163
|
+
} as unknown as ConstructorParameters<typeof ChatInputManager>[0];
|
|
164
|
+
const sessionListManager = {
|
|
165
|
+
ensureDraftSession: vi.fn(() => 'materialized-draft-session'),
|
|
166
|
+
} as unknown as ConstructorParameters<typeof ChatInputManager>[1];
|
|
167
|
+
const manager = new ChatInputManager(
|
|
168
|
+
chatRunManager,
|
|
169
|
+
sessionListManager,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await manager.send();
|
|
173
|
+
|
|
174
|
+
expect(chatRunManager.sendMessage).toHaveBeenCalledWith(
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
projectRoot: '/Users/demo/.nextclaw/workspace',
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
141
181
|
it('does not send while the runtime is still blocked during startup', async () => {
|
|
142
182
|
useChatInputStore.setState({
|
|
143
183
|
snapshot: {
|
|
@@ -251,6 +291,11 @@ describe('ChatInputManager', () => {
|
|
|
251
291
|
expect(useChatInputStore.getState().snapshot.composerNodes).toEqual(originalComposerNodes);
|
|
252
292
|
});
|
|
253
293
|
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('ChatInputManager composer focus', () => {
|
|
297
|
+
beforeEach(resetChatInputManagerStoreState);
|
|
298
|
+
|
|
254
299
|
it('creates and consumes one-shot composer focus requests', () => {
|
|
255
300
|
const manager = new ChatInputManager(
|
|
256
301
|
{} as ConstructorParameters<typeof ChatInputManager>[0],
|
|
@@ -268,6 +313,26 @@ describe('ChatInputManager', () => {
|
|
|
268
313
|
manager.consumeComposerFocusRequest(request!.id);
|
|
269
314
|
expect(useChatInputStore.getState().snapshot.composerFocusRequest).toBeNull();
|
|
270
315
|
});
|
|
316
|
+
|
|
317
|
+
it('applies prompt suggestions to the composer and requests focus', () => {
|
|
318
|
+
const manager = new ChatInputManager(
|
|
319
|
+
{} as ConstructorParameters<typeof ChatInputManager>[0],
|
|
320
|
+
{} as ConstructorParameters<typeof ChatInputManager>[1],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
manager.applyPromptSuggestion(' Build a release plan ');
|
|
324
|
+
|
|
325
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
326
|
+
draft: 'Build a release plan',
|
|
327
|
+
composerNodes: [
|
|
328
|
+
expect.objectContaining({
|
|
329
|
+
type: 'text',
|
|
330
|
+
text: 'Build a release plan',
|
|
331
|
+
}),
|
|
332
|
+
],
|
|
333
|
+
composerFocusRequest: { id: 1, placement: 'end' },
|
|
334
|
+
});
|
|
335
|
+
});
|
|
271
336
|
});
|
|
272
337
|
|
|
273
338
|
describe('ChatInputManager configuration sync', () => {
|
|
@@ -29,6 +29,7 @@ import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
|
|
|
29
29
|
import { normalizeSessionType } from '@/features/chat/features/session-type/utils/chat-session-type.utils';
|
|
30
30
|
import { systemStatusManager } from '@/features/system-status';
|
|
31
31
|
import { shouldClearPendingProjectRootOverride } from '@/features/chat/features/session/utils/chat-run-metadata.utils';
|
|
32
|
+
import { normalizeSessionProjectRootValue } from '@/shared/lib/session-project';
|
|
32
33
|
|
|
33
34
|
function resolveModelForSend(value: string): string | undefined {
|
|
34
35
|
return value || undefined;
|
|
@@ -142,15 +143,19 @@ export class ChatInputManager {
|
|
|
142
143
|
sessionKey: string | null | undefined;
|
|
143
144
|
selectedSessionProjectRoot: string | null | undefined;
|
|
144
145
|
}): string | null => {
|
|
146
|
+
const { sessionKey, selectedSessionProjectRoot } = params;
|
|
145
147
|
const { pendingProjectRoot, pendingProjectRootSessionKey } =
|
|
146
148
|
useChatInputStore.getState().snapshot;
|
|
147
149
|
if (
|
|
148
150
|
pendingProjectRoot !== null &&
|
|
149
|
-
(!
|
|
151
|
+
(!sessionKey || sessionKey === pendingProjectRootSessionKey)
|
|
150
152
|
) {
|
|
151
153
|
return pendingProjectRoot;
|
|
152
154
|
}
|
|
153
|
-
|
|
155
|
+
if (!sessionKey) {
|
|
156
|
+
return useChatInputStore.getState().snapshot.defaultProjectRoot;
|
|
157
|
+
}
|
|
158
|
+
return selectedSessionProjectRoot ?? null;
|
|
154
159
|
};
|
|
155
160
|
|
|
156
161
|
clearPendingProjectRootOverrideForSession = (params: {
|
|
@@ -194,6 +199,15 @@ export class ChatInputManager {
|
|
|
194
199
|
this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
|
|
195
200
|
};
|
|
196
201
|
|
|
202
|
+
applyPromptSuggestion = (prompt: string) => {
|
|
203
|
+
const normalizedPrompt = prompt.trim();
|
|
204
|
+
if (!normalizedPrompt) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.setDraft(normalizedPrompt);
|
|
208
|
+
this.requestComposerFocusAtEnd();
|
|
209
|
+
};
|
|
210
|
+
|
|
197
211
|
requestComposerFocusAtEnd = () => {
|
|
198
212
|
const currentRequest = useChatInputStore.getState().snapshot.composerFocusRequest;
|
|
199
213
|
useChatInputStore.getState().setSnapshot({
|
|
@@ -257,6 +271,14 @@ export class ChatInputManager {
|
|
|
257
271
|
});
|
|
258
272
|
};
|
|
259
273
|
|
|
274
|
+
setPendingProjectRoot = (projectRoot: string | null) => {
|
|
275
|
+
const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
|
|
276
|
+
useChatInputStore.getState().setSnapshot({
|
|
277
|
+
pendingProjectRoot: normalizedProjectRoot,
|
|
278
|
+
pendingProjectRootSessionKey: null,
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
260
282
|
send = async () => {
|
|
261
283
|
const inputSnapshot = useChatInputStore.getState().snapshot;
|
|
262
284
|
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
@@ -19,6 +19,7 @@ export type ChatInputSnapshot = {
|
|
|
19
19
|
pendingSessionType: string;
|
|
20
20
|
pendingProjectRoot: string | null;
|
|
21
21
|
pendingProjectRootSessionKey: string | null;
|
|
22
|
+
defaultProjectRoot: string | null;
|
|
22
23
|
defaultSessionType: string;
|
|
23
24
|
canStopGeneration: boolean;
|
|
24
25
|
stopDisabledReason: string | null;
|
|
@@ -69,6 +70,7 @@ const initialSnapshot: ChatInputSnapshot = {
|
|
|
69
70
|
pendingSessionType: 'native',
|
|
70
71
|
pendingProjectRoot: null,
|
|
71
72
|
pendingProjectRootSessionKey: null,
|
|
73
|
+
defaultProjectRoot: null,
|
|
72
74
|
defaultSessionType: 'native',
|
|
73
75
|
canStopGeneration: false,
|
|
74
76
|
stopDisabledReason: null,
|
|
@@ -37,6 +37,11 @@ export type ChatInputBarAdapterTexts = {
|
|
|
37
37
|
modelNoOptionsLabel: string;
|
|
38
38
|
recentModelsLabel: string;
|
|
39
39
|
allModelsLabel: string;
|
|
40
|
+
favoriteModelsLabel: string;
|
|
41
|
+
modelSearchPlaceholder: string;
|
|
42
|
+
modelSearchEmptyLabel: string;
|
|
43
|
+
favoriteModelLabel: string;
|
|
44
|
+
unfavoriteModelLabel: string;
|
|
40
45
|
thinkingLabels: Record<ChatThinkingLevel, string>;
|
|
41
46
|
noModelOptionsLabel: string;
|
|
42
47
|
configureProviderLabel: string;
|