@nextclaw/ui 0.14.1-beta.0 → 0.14.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/assets/{api-C6EDHBDR.js → api-DPcvCLfa.js} +3 -3
  3. package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-D6jtziu4.js} +2 -2
  4. package/dist/assets/chat-page-BBnNJoKG.js +105 -0
  5. package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-BCJhqdK9.js} +1 -1
  6. package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-CWh5FfK2.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-DsRhBizi.js} +1 -1
  8. package/dist/assets/{dist-CmMro3nV.js → dist-DPrgcEC0.js} +1 -1
  9. package/dist/assets/{dist-BIYV2-Ia.js → dist-DsE4OsSS.js} +1 -1
  10. package/dist/assets/doc-browser-C8xOF_9F.js +1 -0
  11. package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DgQedeAn.js} +1 -1
  12. package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-B46tQGmO.js} +1 -1
  13. package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-B81FStLm.js} +1 -1
  14. package/dist/assets/{external-link-D2cBOCqn.js → external-link-C_dmG-WJ.js} +1 -1
  15. package/dist/assets/index-7o1WdUbU.js +103 -0
  16. package/dist/assets/index-DQWmEWzk.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +40 -0
  19. package/dist/assets/model-config-page-Cv0O3BIj.js +1 -0
  20. package/dist/assets/plus-Bnt56oqG.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +1 -0
  22. package/dist/assets/providers-config-page-CPDz0Lxc.js +1 -0
  23. package/dist/assets/{react-DYQXXGbH.js → react-sZz5HSCm.js} +1 -1
  24. package/dist/assets/remote-B0LMs8u7.js +1 -0
  25. package/dist/assets/rotate-cw-BfyG_KEF.js +1 -0
  26. package/dist/assets/runtime-config-page-q0fvv8ME.js +1 -0
  27. package/dist/assets/{save-BNVTgBVn.js → save-MzjoiHri.js} +1 -1
  28. package/dist/assets/{search-CKCzwLId.js → search-B29dE4GN.js} +1 -1
  29. package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-Dl5sZzob.js} +1 -1
  30. package/dist/assets/secrets-config-page-C_l-IFCK.js +3 -0
  31. package/dist/assets/{select-CIRwTNP4.js → select-BKmh05hd.js} +2 -2
  32. package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-BS_7Ueom.js} +1 -1
  33. package/dist/assets/use-config-DZkBWIdL.js +1 -0
  34. package/dist/assets/x-CW2KO-Hz.js +1 -0
  35. package/dist/index.html +14 -14
  36. package/package.json +9 -9
  37. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  38. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  39. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  40. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +70 -7
  41. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +104 -16
  42. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +88 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
  47. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  48. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
  50. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  51. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  52. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  53. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
  54. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  55. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  56. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  57. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +3 -2
  58. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  59. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  60. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +24 -0
  61. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +27 -1
  62. package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +17 -4
  63. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +15 -3
  64. package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
  65. package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
  66. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  67. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  68. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  69. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  70. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  71. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  72. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  73. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  74. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  75. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  76. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  77. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  78. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  79. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  80. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
  81. package/src/features/chat/managers/chat-input.manager.ts +24 -2
  82. package/src/features/chat/stores/chat-input.store.ts +2 -0
  83. package/src/features/chat/types/chat-input-bar.types.ts +5 -0
  84. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
  85. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
  86. package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
  87. package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
  88. package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
  89. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  90. package/src/features/settings/components/config/provider-form.tsx +284 -387
  91. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  92. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  93. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  94. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  95. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  96. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  97. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  98. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  99. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  100. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  101. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  102. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  103. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  104. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  105. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  106. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  107. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  108. package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
  109. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  110. package/src/shared/components/common/agent-avatar.tsx +5 -2
  111. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  112. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  113. package/src/shared/components/ui/README.md +7 -0
  114. package/src/shared/components/ui/popover.tsx +19 -3
  115. package/src/shared/components/ui/select.tsx +14 -4
  116. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  117. package/src/shared/lib/api/index.ts +3 -0
  118. package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
  119. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  120. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  121. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
  122. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  123. package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
  124. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
  125. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
  127. package/dist/assets/chat-page-BXngLJY6.js +0 -105
  128. package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
  129. package/dist/assets/index-DTb_XVIH.js +0 -103
  130. package/dist/assets/index-ayiPYhf1.css +0 -1
  131. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
  132. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
  133. package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
  134. package/dist/assets/plus-CYE1onU2.js +0 -1
  135. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
  136. package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
  137. package/dist/assets/remote-BlJDbXOI.js +0 -1
  138. package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
  139. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
  140. package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
  141. package/dist/assets/use-config-DgKM9VIU.js +0 -1
  142. package/dist/assets/x-Cm3JjYut.js +0 -1
  143. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  144. 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
+ });
@@ -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
+ });
@@ -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
- (!params.sessionKey || params.sessionKey === pendingProjectRootSessionKey)
151
+ (!sessionKey || sessionKey === pendingProjectRootSessionKey)
150
152
  ) {
151
153
  return pendingProjectRoot;
152
154
  }
153
- return params.selectedSessionProjectRoot ?? null;
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;
@@ -236,6 +236,30 @@ describe("MarketplacePage", () => {
236
236
  expect(card?.textContent).not.toContain("Skill");
237
237
  });
238
238
 
239
+ it("hides redundant skill tags while keeping useful tags", () => {
240
+ mocks.itemsQuery = createItemsQuery({
241
+ data: {
242
+ total: 1,
243
+ page: 1,
244
+ pageSize: 20,
245
+ totalPages: 1,
246
+ sort: "relevance",
247
+ items: [
248
+ createMarketplaceItem({
249
+ tags: ["skill", "automation", "Skill", "ops"],
250
+ }),
251
+ ],
252
+ } satisfies MarketplaceListView,
253
+ });
254
+
255
+ render(<MarketplacePage forcedType="skills" />);
256
+
257
+ expect(screen.queryByText("skill")).toBeNull();
258
+ expect(screen.queryByText("Skill")).toBeNull();
259
+ expect(screen.getAllByText("automation").length).toBeGreaterThan(0);
260
+ expect(screen.getAllByText("ops").length).toBeGreaterThan(0);
261
+ });
262
+
239
263
  it("does not dim the loaded list during background refresh", () => {
240
264
  mocks.itemsQuery = createItemsQuery({
241
265
  data: {
@@ -46,7 +46,7 @@ export function SkillShelfCard(props: {
46
46
  layout === "rail" ? "w-[260px] shrink-0" : "w-full min-w-0",
47
47
  )}
48
48
  >
49
- <div>
49
+ <div className="flex min-h-0 flex-1 flex-col">
50
50
  <div className="mb-2.5 flex min-w-0 items-start gap-2.5">
51
51
  <MarketplaceItemIcon name={item.name} fallback={item.install.spec} />
52
52
  <div className="min-w-0 flex-1 pt-0.5">
@@ -97,17 +97,22 @@ function formatShelfMeta(item: MarketplaceItemSummary) {
97
97
  }
98
98
 
99
99
  function TagLine({ tags }: { tags: string[] }) {
100
- const visibleTags = tags.slice(0, 2);
100
+ const visibleTags = tags.filter(isVisibleShelfTag).slice(0, 2);
101
101
  if (visibleTags.length === 0) {
102
102
  return null;
103
103
  }
104
104
  return (
105
- <div className="mt-2 truncate text-[11px] font-medium text-gray-400">
105
+ <div className="mt-auto truncate pt-2 text-[11px] font-medium text-gray-400">
106
106
  {visibleTags.join(" / ")}
107
107
  </div>
108
108
  );
109
109
  }
110
110
 
111
+ function isVisibleShelfTag(tag: string) {
112
+ const normalized = tag.trim().toLowerCase();
113
+ return normalized.length > 0 && normalized !== "skill";
114
+ }
115
+
111
116
  function formatUpdatedAt(value: string) {
112
117
  const date = value.slice(0, 10);
113
118
  return date || value;