@nextclaw/ui 0.14.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-B50yVdT8.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/channels-list-page-DfpjB8XB.js +8 -0
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/config-split-page-CeFfbaqc.js +1 -0
  6. package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/desktop-update-config-CcK6jiEb.js +1 -0
  8. package/dist/assets/{dist-D19wiQwB.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-NZRV-BxD.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/doc-browser-DVHhEb96.js +1 -0
  12. package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-528k6MDD.js → external-link-CcgtBK26.js} +1 -1
  15. package/dist/assets/index-ByJCbQqr.css +1 -0
  16. package/dist/assets/index-DjXqiloY.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
  19. package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
  20. package/dist/assets/plus-1Rxl5FlV.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
  22. package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
  23. package/dist/assets/{react-Dsotw5vM.js → react-CTqdhFMk.js} +1 -1
  24. package/dist/assets/remote-fQ7N_r8u.js +1 -0
  25. package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
  26. package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
  27. package/dist/assets/{save-CHDnDTok.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/search-D5FQ5q0X.js +1 -0
  29. package/dist/assets/search-config-page-AMEJJNIS.js +1 -0
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/select-Cu5n0E-3.js +41 -0
  32. package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-C5aOtjxu.js} +1 -1
  33. package/dist/assets/use-config-D8TESeo6.js +1 -0
  34. package/dist/assets/x-1qETdQ70.js +1 -0
  35. package/dist/index.html +14 -27
  36. package/package.json +9 -9
  37. package/src/app/presenters/app.presenter.ts +2 -0
  38. package/src/features/account/components/account-panel.tsx +2 -2
  39. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
  40. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  41. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
  42. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +122 -85
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +120 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +66 -41
  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 +22 -36
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -52
  50. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
  51. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
  52. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
  53. package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
  54. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  55. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  57. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  58. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
  59. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
  60. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +14 -9
  61. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  62. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  63. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  64. package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
  65. package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
  66. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
  67. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  68. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
  69. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
  70. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  71. package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
  72. package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
  73. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
  74. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
  75. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  76. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  77. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
  78. package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
  79. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  80. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  81. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  82. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  83. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  84. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  85. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  86. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  87. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  88. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  89. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  90. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  91. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  92. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  93. package/src/features/chat/index.ts +1 -0
  94. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +93 -0
  95. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
  96. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
  97. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
  98. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
  99. package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
  100. package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
  101. package/src/features/chat/managers/chat-input.manager.ts +37 -15
  102. package/src/features/chat/managers/chat-run.manager.ts +3 -0
  103. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
  104. package/src/features/chat/managers/chat-thread.manager.ts +4 -0
  105. package/src/features/chat/managers/chat-ui.manager.ts +2 -6
  106. package/src/features/chat/managers/recent-selection.manager.ts +21 -67
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
  108. package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
  109. package/src/features/chat/stores/chat-input.store.ts +9 -1
  110. package/src/features/chat/stores/recent-selection.store.ts +85 -0
  111. package/src/features/chat/types/chat-input-bar.types.ts +5 -1
  112. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  113. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
  115. package/src/features/panel-apps/index.ts +2 -0
  116. package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
  117. package/src/features/settings/components/config/provider-form.tsx +284 -387
  118. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  119. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  120. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  121. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  122. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  123. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  124. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  125. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  126. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  127. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  128. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  129. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  130. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  132. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  133. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  134. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  135. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  136. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  137. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  138. package/src/shared/components/ui/README.md +7 -0
  139. package/src/shared/components/ui/popover.tsx +19 -3
  140. package/src/shared/components/ui/select.tsx +14 -4
  141. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  142. package/src/shared/lib/api/chat-session-type.types.ts +7 -0
  143. package/src/shared/lib/api/index.ts +2 -0
  144. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  145. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  146. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  147. package/src/shared/lib/api/types.ts +3 -0
  148. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -1
  149. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  150. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  151. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -1
  152. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  153. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  154. package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
  155. package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
  156. package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
  157. package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
  158. package/dist/assets/chat-page-CO27cl4v.js +0 -105
  159. package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
  160. package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
  161. package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
  162. package/dist/assets/doc-browser-BjHUczQb.js +0 -1
  163. package/dist/assets/es2015-BUIIKCNh.js +0 -41
  164. package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
  165. package/dist/assets/index-CMNrRhnG.js +0 -101
  166. package/dist/assets/index-UTmTW1eU.css +0 -1
  167. package/dist/assets/key-round-D3vD9dj7.js +0 -1
  168. package/dist/assets/loader-circle-DV9TSRns.js +0 -1
  169. package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
  170. package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
  171. package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
  172. package/dist/assets/notice-card-qqlbsOoR.js +0 -1
  173. package/dist/assets/plus-Bf9qhPDO.js +0 -1
  174. package/dist/assets/popover-BnmPOXe3.js +0 -1
  175. package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
  176. package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
  177. package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
  178. package/dist/assets/remote-DNMEy_c4.js +0 -1
  179. package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
  180. package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
  181. package/dist/assets/search-D7L-TCH-.js +0 -1
  182. package/dist/assets/search-config-page-CA3RVAef.js +0 -1
  183. package/dist/assets/secrets-config-page-95yaH1fa.js +0 -3
  184. package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
  185. package/dist/assets/tabs-CJvw5Lqp.js +0 -1
  186. package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
  187. package/dist/assets/trash-2-CSfX-BvU.js +0 -1
  188. package/dist/assets/use-config-TzwzTgg0.js +0 -1
  189. package/dist/assets/x-fkIWvi07.js +0 -1
  190. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  191. package/src/features/chat/components/chat-welcome.tsx +0 -102
  192. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
  193. package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
  194. /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
@@ -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
+ }
@@ -3,6 +3,7 @@ export { ChatSidebar } from "./components/layout/chat-sidebar";
3
3
  export { ChatPresenterProvider } from "./components/providers/chat-presenter.provider";
4
4
  export { usePresenter } from "./components/providers/chat-presenter.provider";
5
5
  export { ChatPresenter } from "./presenters/chat.presenter";
6
+ export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
6
7
  export { useChatInputStore } from "./stores/chat-input.store";
7
8
  export { useChatSessionListStore } from "./stores/chat-session-list.store";
8
9
  export { useChatThreadStore } from "./stores/chat-thread.store";
@@ -1,4 +1,5 @@
1
1
  import { createChatComposerTextNode } from '@nextclaw/agent-chat-ui';
2
+ import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
2
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
4
  import { ChatInputManager } from '@/features/chat/managers/chat-input.manager';
4
5
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
@@ -137,6 +138,46 @@ describe('ChatInputManager', () => {
137
138
  );
138
139
  });
139
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
+
140
181
  it('does not send while the runtime is still blocked during startup', async () => {
141
182
  useChatInputStore.setState({
142
183
  snapshot: {
@@ -250,6 +291,11 @@ describe('ChatInputManager', () => {
250
291
  expect(useChatInputStore.getState().snapshot.composerNodes).toEqual(originalComposerNodes);
251
292
  });
252
293
 
294
+ });
295
+
296
+ describe('ChatInputManager composer focus', () => {
297
+ beforeEach(resetChatInputManagerStoreState);
298
+
253
299
  it('creates and consumes one-shot composer focus requests', () => {
254
300
  const manager = new ChatInputManager(
255
301
  {} as ConstructorParameters<typeof ChatInputManager>[0],
@@ -267,6 +313,26 @@ describe('ChatInputManager', () => {
267
313
  manager.consumeComposerFocusRequest(request!.id);
268
314
  expect(useChatInputStore.getState().snapshot.composerFocusRequest).toBeNull();
269
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
+ });
270
336
  });
271
337
 
272
338
  describe('ChatInputManager configuration sync', () => {
@@ -322,6 +388,33 @@ describe('ChatInputManager configuration sync', () => {
322
388
  });
323
389
  });
324
390
 
391
+ it('sends the runtime-default sentinel so the backend can ignore stale session model metadata', async () => {
392
+ useChatInputStore.setState({
393
+ snapshot: {
394
+ ...useChatInputStore.getState().snapshot,
395
+ selectedSessionType: 'codex',
396
+ selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
397
+ },
398
+ });
399
+ const chatRunManager = {
400
+ sendMessage: vi.fn().mockResolvedValue(undefined),
401
+ stopCurrentRun: vi.fn().mockResolvedValue(undefined),
402
+ } as unknown as ConstructorParameters<typeof ChatInputManager>[0];
403
+ const sessionListManager = {
404
+ ensureDraftSession: vi.fn(() => 'draft-session'),
405
+ } as unknown as ConstructorParameters<typeof ChatInputManager>[1];
406
+ const manager = new ChatInputManager(chatRunManager, sessionListManager);
407
+
408
+ await manager.send();
409
+
410
+ expect(chatRunManager.sendMessage).toHaveBeenCalledWith(
411
+ expect.objectContaining({
412
+ sessionType: 'codex',
413
+ model: RUNTIME_DEFAULT_MODEL_VALUE,
414
+ }),
415
+ );
416
+ });
417
+
325
418
  it('resolves and clears pending project root overrides inside the input manager', () => {
326
419
  useChatInputStore.setState({
327
420
  snapshot: {
@@ -0,0 +1,129 @@
1
+ import type { NcpMessage } from '@nextclaw/ncp';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
4
+ import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
5
+ import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
6
+
7
+ function createChatRunManager() {
8
+ const uiManager = {
9
+ isAtChatRoot: vi.fn(() => true),
10
+ goToSession: vi.fn(),
11
+ };
12
+ return new ChatRunManager(
13
+ uiManager as unknown as ConstructorParameters<typeof ChatRunManager>[0],
14
+ );
15
+ }
16
+
17
+ describe('ChatRunManager run snapshots', () => {
18
+ beforeEach(() => {
19
+ useChatInputStore.setState({
20
+ snapshot: {
21
+ ...useChatInputStore.getState().snapshot,
22
+ canStopGeneration: false,
23
+ sendError: null,
24
+ isSending: false,
25
+ },
26
+ });
27
+ useChatThreadStore.setState({
28
+ snapshot: {
29
+ ...useChatThreadStore.getState().snapshot,
30
+ sessionKey: null,
31
+ isHistoryLoading: false,
32
+ messages: [],
33
+ isSending: false,
34
+ isAwaitingAssistantOutput: false,
35
+ contextWindow: null,
36
+ },
37
+ });
38
+ });
39
+
40
+ it('syncs the run snapshot into chat input and thread state', () => {
41
+ const manager = createChatRunManager();
42
+ const message: NcpMessage = {
43
+ id: 'message-1',
44
+ sessionId: 'session-1',
45
+ role: 'assistant',
46
+ status: 'final',
47
+ parts: [{ type: 'text', text: 'done' }],
48
+ timestamp: '2026-06-10T00:00:00.000Z',
49
+ };
50
+
51
+ manager.applyRunSnapshot({
52
+ routeSessionKey: 'session-1',
53
+ isHydrating: true,
54
+ isSending: false,
55
+ isRunning: true,
56
+ visibleMessages: [message],
57
+ contextWindow: {
58
+ usedContextTokens: 10,
59
+ totalContextTokens: 100,
60
+ availableContextTokens: 90,
61
+ prunedUsedContextTokens: 10,
62
+ droppedHistoryCount: 0,
63
+ truncatedToolResultCount: 0,
64
+ truncatedSystemPrompt: false,
65
+ truncatedUserMessage: false,
66
+ compacted: false,
67
+ compactedUsedContextTokens: 10,
68
+ compactedMessageCount: 0,
69
+ updatedAt: '2026-06-10T00:00:00.000Z',
70
+ },
71
+ sendErrorMessage: 'failed',
72
+ materializedSessionKey: null,
73
+ });
74
+
75
+ expect(useChatInputStore.getState().snapshot).toMatchObject({
76
+ canStopGeneration: true,
77
+ sendError: 'failed',
78
+ isSending: true,
79
+ });
80
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
81
+ sessionKey: 'session-1',
82
+ isHistoryLoading: true,
83
+ messages: [message],
84
+ isSending: true,
85
+ isAwaitingAssistantOutput: true,
86
+ contextWindow: {
87
+ usedContextTokens: 10,
88
+ totalContextTokens: 100,
89
+ },
90
+ });
91
+ });
92
+
93
+ it('resets stale context window state when switching sessions', () => {
94
+ const manager = createChatRunManager();
95
+ useChatThreadStore.getState().setSnapshot({
96
+ sessionKey: 'session-previous',
97
+ contextWindow: {
98
+ usedContextTokens: 80,
99
+ totalContextTokens: 100,
100
+ availableContextTokens: 20,
101
+ prunedUsedContextTokens: 80,
102
+ droppedHistoryCount: 0,
103
+ truncatedToolResultCount: 0,
104
+ truncatedSystemPrompt: false,
105
+ truncatedUserMessage: false,
106
+ compacted: false,
107
+ compactedUsedContextTokens: 80,
108
+ compactedMessageCount: 0,
109
+ updatedAt: '2026-06-10T00:00:00.000Z',
110
+ },
111
+ });
112
+
113
+ manager.applyRunSnapshot({
114
+ routeSessionKey: 'session-next',
115
+ isHydrating: true,
116
+ isSending: false,
117
+ isRunning: false,
118
+ visibleMessages: [],
119
+ contextWindow: null,
120
+ sendErrorMessage: null,
121
+ materializedSessionKey: null,
122
+ });
123
+
124
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
125
+ sessionKey: 'session-next',
126
+ contextWindow: null,
127
+ });
128
+ });
129
+ });
@@ -1,4 +1,5 @@
1
1
  import type { NcpMessage } from '@nextclaw/ncp';
2
+ import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
2
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
4
  import { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
4
5
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
@@ -75,6 +76,39 @@ describe('ChatRunManager', () => {
75
76
  expect(uiManager.goToSession).toHaveBeenCalledWith('materialized-session', { replace: true });
76
77
  });
77
78
 
79
+ it('keeps the runtime-default sentinel in request metadata to override stale session model metadata', async () => {
80
+ const { manager } = createChatRunManager();
81
+ const sendEnvelope = vi.fn(async () => ({
82
+ sessionId: 'session-1',
83
+ userMessageId: 'user-message-1',
84
+ assistantMessageId: null,
85
+ runId: 'run-1',
86
+ }));
87
+ manager.setActiveRuntime({
88
+ sessionKey: 'session-1',
89
+ sendEnvelope,
90
+ abortCurrentRun: vi.fn(),
91
+ resumeCurrentSessionRun: vi.fn(),
92
+ });
93
+
94
+ await manager.sendMessage({
95
+ message: 'hello',
96
+ sessionKey: 'session-1',
97
+ agentId: 'main',
98
+ sessionType: 'codex',
99
+ model: RUNTIME_DEFAULT_MODEL_VALUE,
100
+ });
101
+
102
+ expect(sendEnvelope).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ metadata: expect.objectContaining({
105
+ model: RUNTIME_DEFAULT_MODEL_VALUE,
106
+ preferred_model: RUNTIME_DEFAULT_MODEL_VALUE,
107
+ }),
108
+ }),
109
+ );
110
+ });
111
+
78
112
  it('does not send through a runtime for another session', async () => {
79
113
  const { manager } = createChatRunManager();
80
114
  const sendEnvelope = vi.fn();
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
2
3
  import { updateNcpSession } from '@/shared/lib/api';
3
- import { ChatSessionPreferenceSync } from '../chat-session-preference-sync.manager';
4
+ import { ChatSessionPreferenceSync } from '@/features/chat/managers/chat-session-preference-sync.manager';
4
5
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
5
6
  import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
6
7
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
@@ -25,6 +26,7 @@ describe('ChatSessionPreferenceSync', () => {
25
26
  useChatInputStore.setState((state) => ({
26
27
  snapshot: {
27
28
  ...state.snapshot,
29
+ modelOptions: [],
28
30
  selectedModel: '',
29
31
  selectedThinkingLevel: null
30
32
  }
@@ -74,4 +76,144 @@ describe('ChatSessionPreferenceSync', () => {
74
76
  });
75
77
  });
76
78
  });
79
+
80
+ it('clears the persisted model preference when runtime default is selected', async () => {
81
+ useChatInputStore.setState((state) => ({
82
+ snapshot: {
83
+ ...state.snapshot,
84
+ selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
85
+ selectedThinkingLevel: null
86
+ }
87
+ }));
88
+ useChatSessionListStore.setState((state) => ({
89
+ snapshot: {
90
+ ...state.snapshot,
91
+ selectedSessionKey: 'session-1'
92
+ }
93
+ }));
94
+ useChatThreadStore.setState((state) => ({
95
+ snapshot: {
96
+ ...state.snapshot,
97
+ canDeleteSession: true
98
+ }
99
+ }));
100
+
101
+ const sync = new ChatSessionPreferenceSync(updateNcpSession);
102
+ sync.syncSelectedSessionPreferences();
103
+ await vi.waitFor(() => {
104
+ expect(updateNcpSession).toHaveBeenCalledWith('session-1', {
105
+ preferredModel: null,
106
+ preferredThinking: null
107
+ });
108
+ });
109
+ });
110
+
111
+ it('uses the runtime-default thinking capability default when no session thinking is persisted', () => {
112
+ useChatInputStore.setState((state) => ({
113
+ snapshot: {
114
+ ...state.snapshot,
115
+ modelOptions: [
116
+ {
117
+ value: RUNTIME_DEFAULT_MODEL_VALUE,
118
+ modelLabel: 'Runtime default',
119
+ providerLabel: '',
120
+ isRuntimeDefault: true,
121
+ thinkingCapability: {
122
+ supported: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'],
123
+ default: 'high',
124
+ },
125
+ },
126
+ ],
127
+ selectedModel: '',
128
+ selectedThinkingLevel: null,
129
+ }
130
+ }));
131
+
132
+ const sync = new ChatSessionPreferenceSync(updateNcpSession);
133
+ sync.syncInputSelection({
134
+ selectedSessionExists: true,
135
+ defaultModel: RUNTIME_DEFAULT_MODEL_VALUE,
136
+ selectedSessionPreferredThinking: null,
137
+ fallbackPreferredThinking: null,
138
+ });
139
+
140
+ expect(useChatInputStore.getState().snapshot).toMatchObject({
141
+ selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
142
+ selectedThinkingLevel: 'high',
143
+ });
144
+ });
145
+
146
+ it('does not rewrite the input snapshot when the resolved selection is unchanged', () => {
147
+ useChatInputStore.setState((state) => ({
148
+ snapshot: {
149
+ ...state.snapshot,
150
+ modelOptions: [
151
+ {
152
+ value: 'openai/gpt-5',
153
+ modelLabel: 'GPT-5',
154
+ providerLabel: 'OpenAI',
155
+ thinkingCapability: null,
156
+ },
157
+ ],
158
+ selectedModel: 'openai/gpt-5',
159
+ selectedThinkingLevel: null,
160
+ }
161
+ }));
162
+ const sync = new ChatSessionPreferenceSync(updateNcpSession);
163
+ const listener = vi.fn();
164
+ const unsubscribe = useChatInputStore.subscribe(listener);
165
+
166
+ sync.syncInputSelection({
167
+ selectedSessionExists: true,
168
+ selectedSessionPreferredModel: 'openai/gpt-5',
169
+ selectedSessionPreferredThinking: null,
170
+ });
171
+
172
+ unsubscribe();
173
+ expect(listener).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it('applies a historical session preferred model after its summary arrives', () => {
177
+ useChatInputStore.setState((state) => ({
178
+ snapshot: {
179
+ ...state.snapshot,
180
+ modelOptions: [
181
+ {
182
+ value: 'anthropic/claude-sonnet-4',
183
+ modelLabel: 'Claude Sonnet 4',
184
+ providerLabel: 'Anthropic',
185
+ thinkingCapability: null,
186
+ },
187
+ {
188
+ value: 'openai/gpt-5',
189
+ modelLabel: 'GPT-5',
190
+ providerLabel: 'OpenAI',
191
+ thinkingCapability: null,
192
+ },
193
+ ],
194
+ selectedModel: 'anthropic/claude-sonnet-4',
195
+ selectedThinkingLevel: null,
196
+ }
197
+ }));
198
+ const sync = new ChatSessionPreferenceSync(updateNcpSession);
199
+
200
+ sync.syncInputSelection({
201
+ selectedSessionKey: 'session-2',
202
+ selectedSessionExists: false,
203
+ fallbackPreferredModel: 'anthropic/claude-sonnet-4',
204
+ });
205
+ expect(useChatInputStore.getState().snapshot.selectedModel).toBe(
206
+ 'anthropic/claude-sonnet-4',
207
+ );
208
+
209
+ sync.syncInputSelection({
210
+ selectedSessionKey: 'session-2',
211
+ selectedSessionExists: true,
212
+ selectedSessionPreferredModel: 'openai/gpt-5',
213
+ selectedSessionPreferredThinking: null,
214
+ fallbackPreferredModel: 'anthropic/claude-sonnet-4',
215
+ });
216
+
217
+ expect(useChatInputStore.getState().snapshot.selectedModel).toBe('openai/gpt-5');
218
+ });
77
219
  });