@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
  6. package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
  8. package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
  12. package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-D2cBOCqn.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-DYQXXGbH.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-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
  29. package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
  32. package/dist/assets/{tag-chip-DKhVFtTE.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 -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/features/input/components/chat-input-bar.container.tsx +84 -6
  41. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
  42. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  43. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
  44. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  45. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
  46. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
  47. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  48. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  49. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  50. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
  51. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  52. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  53. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  54. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  55. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  56. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  57. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  58. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  59. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  60. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  61. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  62. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  63. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  64. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  65. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  66. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  67. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  68. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  69. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  70. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  71. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  72. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
  73. package/src/features/chat/managers/chat-input.manager.ts +24 -2
  74. package/src/features/chat/stores/chat-input.store.ts +2 -0
  75. package/src/features/chat/types/chat-input-bar.types.ts +5 -0
  76. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  77. package/src/features/settings/components/config/provider-form.tsx +284 -387
  78. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  79. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  80. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  81. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  82. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  83. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  84. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  85. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  86. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  87. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  88. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  89. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  90. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  91. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  92. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  93. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  94. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  95. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  96. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  97. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  98. package/src/shared/components/ui/README.md +7 -0
  99. package/src/shared/components/ui/popover.tsx +19 -3
  100. package/src/shared/components/ui/select.tsx +14 -4
  101. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  102. package/src/shared/lib/api/index.ts +2 -0
  103. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  104. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  105. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
  106. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  107. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
  108. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  109. package/dist/assets/chat-page-BXngLJY6.js +0 -105
  110. package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
  111. package/dist/assets/index-DTb_XVIH.js +0 -103
  112. package/dist/assets/index-ayiPYhf1.css +0 -1
  113. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
  115. package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
  116. package/dist/assets/plus-CYE1onU2.js +0 -1
  117. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
  118. package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
  119. package/dist/assets/remote-BlJDbXOI.js +0 -1
  120. package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
  121. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
  122. package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
  123. package/dist/assets/use-config-DgKM9VIU.js +0 -1
  124. package/dist/assets/x-Cm3JjYut.js +0 -1
  125. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  126. package/src/features/chat/components/chat-welcome.tsx +0 -102
@@ -19,14 +19,15 @@ export function createLine(params: {
19
19
  oldLineNumber?: number;
20
20
  newLineNumber?: number;
21
21
  }): ChatFileOperationLineViewModel {
22
+ const { kind, newLineNumber, oldLineNumber, text } = params;
22
23
  return {
23
- kind: params.kind,
24
- text: params.text,
25
- ...(typeof params.oldLineNumber === "number"
26
- ? { oldLineNumber: params.oldLineNumber }
24
+ kind,
25
+ text,
26
+ ...(typeof oldLineNumber === "number"
27
+ ? { oldLineNumber }
27
28
  : {}),
28
- ...(typeof params.newLineNumber === "number"
29
- ? { newLineNumber: params.newLineNumber }
29
+ ...(typeof newLineNumber === "number"
30
+ ? { newLineNumber }
30
31
  : {}),
31
32
  };
32
33
  }
@@ -51,18 +52,19 @@ export function buildPreviewLines(params: {
51
52
  oldStartLine: number;
52
53
  newStartLine: number;
53
54
  }): ChatFileOperationLineViewModel[] {
54
- return splitLines(params.text).map((line, index) =>
55
- params.kind === "add"
55
+ const { kind, newStartLine, oldStartLine, text } = params;
56
+ return splitLines(text).map((line, index) =>
57
+ kind === "add"
56
58
  ? createLine({
57
59
  kind: "add",
58
60
  text: line,
59
- newLineNumber: params.newStartLine + index,
61
+ newLineNumber: newStartLine + index,
60
62
  })
61
63
  : createLine({
62
64
  kind: "context",
63
65
  text: line,
64
- oldLineNumber: params.oldStartLine + index,
65
- newLineNumber: params.newStartLine + index,
66
+ oldLineNumber: oldStartLine + index,
67
+ newLineNumber: newStartLine + index,
66
68
  }),
67
69
  );
68
70
  }
@@ -73,10 +75,11 @@ function buildFallbackDiffLines(params: {
73
75
  oldStartLine?: number;
74
76
  newStartLine?: number;
75
77
  }): ChatFileOperationLineViewModel[] {
76
- let oldLineNumber = params.oldStartLine;
77
- let newLineNumber = params.newStartLine;
78
+ const { afterLines, beforeLines, newStartLine, oldStartLine } = params;
79
+ let oldLineNumber = oldStartLine;
80
+ let newLineNumber = newStartLine;
78
81
  return [
79
- ...params.beforeLines.map((line) => {
82
+ ...beforeLines.map((line) => {
80
83
  const nextLine = createLine({
81
84
  kind: "remove",
82
85
  text: line,
@@ -85,7 +88,7 @@ function buildFallbackDiffLines(params: {
85
88
  oldLineNumber = incrementLineNumber(oldLineNumber);
86
89
  return nextLine;
87
90
  }),
88
- ...params.afterLines.map((line) => {
91
+ ...afterLines.map((line) => {
89
92
  const nextLine = createLine({
90
93
  kind: "add",
91
94
  text: line,
@@ -101,22 +104,23 @@ function buildLcsMatrix(params: {
101
104
  beforeLines: string[];
102
105
  afterLines: string[];
103
106
  }): number[][] {
107
+ const { afterLines, beforeLines } = params;
104
108
  const matrix: number[][] = Array.from(
105
- { length: params.beforeLines.length + 1 },
106
- () => Array.from({ length: params.afterLines.length + 1 }, () => 0),
109
+ { length: beforeLines.length + 1 },
110
+ () => Array.from({ length: afterLines.length + 1 }, () => 0),
107
111
  );
108
112
  for (
109
- let beforeIndex = params.beforeLines.length - 1;
113
+ let beforeIndex = beforeLines.length - 1;
110
114
  beforeIndex >= 0;
111
115
  beforeIndex -= 1
112
116
  ) {
113
117
  for (
114
- let afterIndex = params.afterLines.length - 1;
118
+ let afterIndex = afterLines.length - 1;
115
119
  afterIndex >= 0;
116
120
  afterIndex -= 1
117
121
  ) {
118
122
  matrix[beforeIndex]![afterIndex] =
119
- params.beforeLines[beforeIndex] === params.afterLines[afterIndex]
123
+ beforeLines[beforeIndex] === afterLines[afterIndex]
120
124
  ? (matrix[beforeIndex + 1]![afterIndex + 1] ?? 0) + 1
121
125
  : Math.max(
122
126
  matrix[beforeIndex + 1]![afterIndex] ?? 0,
@@ -136,33 +140,36 @@ function appendRemainingDiffLines(params: {
136
140
  oldLineNumber?: number;
137
141
  newLineNumber?: number;
138
142
  }): void {
143
+ const { afterIndex, afterLines, beforeIndex, beforeLines, lines, newLineNumber: initialNewLineNumber, oldLineNumber: initialOldLineNumber } = params;
144
+ let oldLineNumber = initialOldLineNumber;
145
+ let newLineNumber = initialNewLineNumber;
139
146
  for (
140
- let index = params.beforeIndex;
141
- index < params.beforeLines.length;
147
+ let index = beforeIndex;
148
+ index < beforeLines.length;
142
149
  index += 1
143
150
  ) {
144
- params.lines.push(
151
+ lines.push(
145
152
  createLine({
146
153
  kind: "remove",
147
- text: params.beforeLines[index] ?? "",
148
- oldLineNumber: params.oldLineNumber,
154
+ text: beforeLines[index] ?? "",
155
+ oldLineNumber,
149
156
  }),
150
157
  );
151
- params.oldLineNumber = incrementLineNumber(params.oldLineNumber);
158
+ oldLineNumber = incrementLineNumber(oldLineNumber);
152
159
  }
153
160
  for (
154
- let index = params.afterIndex;
155
- index < params.afterLines.length;
161
+ let index = afterIndex;
162
+ index < afterLines.length;
156
163
  index += 1
157
164
  ) {
158
- params.lines.push(
165
+ lines.push(
159
166
  createLine({
160
167
  kind: "add",
161
- text: params.afterLines[index] ?? "",
162
- newLineNumber: params.newLineNumber,
168
+ text: afterLines[index] ?? "",
169
+ newLineNumber,
163
170
  }),
164
171
  );
165
- params.newLineNumber = incrementLineNumber(params.newLineNumber);
172
+ newLineNumber = incrementLineNumber(newLineNumber);
166
173
  }
167
174
  }
168
175
 
@@ -172,14 +179,15 @@ export function buildLineDiff(params: {
172
179
  oldStartLine?: number;
173
180
  newStartLine?: number;
174
181
  }): ChatFileOperationLineViewModel[] {
175
- const beforeLines = splitLines(params.beforeText);
176
- const afterLines = splitLines(params.afterText);
182
+ const { afterText, beforeText, newStartLine, oldStartLine } = params;
183
+ const beforeLines = splitLines(beforeText);
184
+ const afterLines = splitLines(afterText);
177
185
  if (beforeLines.length * afterLines.length > MAX_DIFF_MATRIX_CELLS) {
178
186
  return buildFallbackDiffLines({
179
187
  beforeLines,
180
188
  afterLines,
181
- oldStartLine: params.oldStartLine,
182
- newStartLine: params.newStartLine,
189
+ oldStartLine,
190
+ newStartLine,
183
191
  });
184
192
  }
185
193
 
@@ -190,8 +198,8 @@ export function buildLineDiff(params: {
190
198
  const lines: ChatFileOperationLineViewModel[] = [];
191
199
  let beforeIndex = 0;
192
200
  let afterIndex = 0;
193
- let oldLineNumber = params.oldStartLine;
194
- let newLineNumber = params.newStartLine;
201
+ let oldLineNumber = oldStartLine;
202
+ let newLineNumber = newStartLine;
195
203
  while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
196
204
  if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
197
205
  lines.push(
@@ -20,10 +20,11 @@ export function buildCaption(params: {
20
20
  operation?: string | null;
21
21
  lines: ChatFileOperationLineViewModel[];
22
22
  }): string | undefined {
23
- const additions = params.lines.filter((line) => line.kind === "add").length;
24
- const deletions = params.lines.filter((line) => line.kind === "remove").length;
23
+ const { lines, operation } = params;
24
+ const additions = lines.filter((line) => line.kind === "add").length;
25
+ const deletions = lines.filter((line) => line.kind === "remove").length;
25
26
  const parts: string[] = [];
26
- const normalizedOperation = params.operation?.trim().toLowerCase() ?? "";
27
+ const normalizedOperation = operation?.trim().toLowerCase() ?? "";
27
28
  if (normalizedOperation && normalizedOperation !== "update") {
28
29
  parts.push(normalizedOperation);
29
30
  }
@@ -46,20 +47,21 @@ export function readDefaultDiffStartLines(params: {
46
47
  oldStartLine?: number;
47
48
  newStartLine?: number;
48
49
  } {
49
- const normalizedOperation = params.operation?.trim().toLowerCase() ?? "";
50
+ const { afterText, beforeText, newStartLine: rawNewStartLine, oldStartLine: rawOldStartLine, operation } = params;
51
+ const normalizedOperation = operation?.trim().toLowerCase() ?? "";
50
52
  const oldStartLine =
51
- typeof params.oldStartLine === "number"
52
- ? params.oldStartLine
53
+ typeof rawOldStartLine === "number"
54
+ ? rawOldStartLine
53
55
  : (normalizedOperation === "delete" ||
54
56
  normalizedOperation === "remove") &&
55
- params.beforeText != null
57
+ beforeText != null
56
58
  ? 1
57
59
  : undefined;
58
60
  const newStartLine =
59
- typeof params.newStartLine === "number"
60
- ? params.newStartLine
61
+ typeof rawNewStartLine === "number"
62
+ ? rawNewStartLine
61
63
  : (normalizedOperation === "write" || normalizedOperation === "add") &&
62
- params.afterText != null
64
+ afterText != null
63
65
  ? 1
64
66
  : undefined;
65
67
  return { oldStartLine, newStartLine };
@@ -83,16 +85,17 @@ export function buildParsedPatchBlock(params: {
83
85
  operation: string | null;
84
86
  lines: ChatFileOperationLineViewModel[];
85
87
  }): ParsedBlock {
86
- const limited = limitLines(params.lines);
88
+ const { lines, operation, path } = params;
89
+ const limited = limitLines(lines);
87
90
  return {
88
- path: params.path,
91
+ path,
89
92
  display: "diff",
90
93
  caption: buildCaption({
91
- operation: params.operation,
92
- lines: params.lines,
94
+ operation,
95
+ lines,
93
96
  }),
94
97
  lines: limited.lines,
95
- ...(limited.truncated ? { fullLines: params.lines } : {}),
98
+ ...(limited.truncated ? { fullLines: lines } : {}),
96
99
  truncated: limited.truncated,
97
100
  };
98
101
  }
@@ -2,6 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { ChatSessionProjectBadge } from '@/features/chat/features/session/components/session-header/chat-session-project-badge';
5
+ import { createPopoverAvailableHeightLimit } from '@/shared/components/ui/popover';
5
6
 
6
7
  const mocks = vi.hoisted(() => ({
7
8
  updateSessionProject: vi.fn(),
@@ -15,6 +16,17 @@ vi.mock('@/features/chat/features/session/components/session-header/chat-session
15
16
  ChatSessionProjectDialog: () => null,
16
17
  }));
17
18
 
19
+ function renderProjectBadge() {
20
+ return render(
21
+ <ChatSessionProjectBadge
22
+ sessionKey="session-1"
23
+ projectName="project-alpha"
24
+ projectRoot="/tmp/project-alpha"
25
+ persistToServer
26
+ />
27
+ );
28
+ }
29
+
18
30
  describe('ChatSessionProjectBadge', () => {
19
31
  beforeEach(() => {
20
32
  mocks.updateSessionProject.mockReset();
@@ -24,31 +36,26 @@ describe('ChatSessionProjectBadge', () => {
24
36
  it('shows project actions inside the badge popover', async () => {
25
37
  const user = userEvent.setup();
26
38
 
27
- render(
28
- <ChatSessionProjectBadge
29
- sessionKey="session-1"
30
- projectName="project-alpha"
31
- projectRoot="/tmp/project-alpha"
32
- persistToServer
33
- />
34
- );
39
+ renderProjectBadge();
35
40
 
36
41
  await user.click(screen.getByRole('button', { name: 'Set Project Directory' }));
37
42
 
38
43
  expect(screen.getAllByText('Set Project Directory').length).toBeGreaterThan(0);
39
44
  expect(screen.getByText('Clear Project Directory')).toBeTruthy();
40
- expect(screen.getByText('/tmp/project-alpha')).toBeTruthy();
45
+ const projectRoot = screen.getByText('/tmp/project-alpha');
46
+ expect(projectRoot).toBeTruthy();
47
+
48
+ const boundedPopover = projectRoot.closest('[style*="max-height"]') as HTMLElement | null;
49
+ expect(boundedPopover?.style.maxHeight).toBe(
50
+ createPopoverAvailableHeightLimit('18rem')
51
+ );
52
+ expect(boundedPopover?.style.maxHeight).toContain('max(0px');
53
+ expect(boundedPopover?.style.maxHeight).toContain('100vh');
54
+ expect(boundedPopover?.style.maxHeight).toContain('2rem');
41
55
  });
42
56
 
43
57
  it('uses the neutral header tag styling instead of a highlighted accent color', () => {
44
- render(
45
- <ChatSessionProjectBadge
46
- sessionKey="session-1"
47
- projectName="project-alpha"
48
- projectRoot="/tmp/project-alpha"
49
- persistToServer
50
- />
51
- );
58
+ renderProjectBadge();
52
59
 
53
60
  const trigger = screen.getByRole('button', { name: 'Set Project Directory' });
54
61
  expect(trigger.className).toContain('border-gray-200');
@@ -59,14 +66,7 @@ describe('ChatSessionProjectBadge', () => {
59
66
  it('clears the current project from the badge popover', async () => {
60
67
  const user = userEvent.setup();
61
68
 
62
- render(
63
- <ChatSessionProjectBadge
64
- sessionKey="session-1"
65
- projectName="project-alpha"
66
- projectRoot="/tmp/project-alpha"
67
- persistToServer
68
- />
69
- );
69
+ renderProjectBadge();
70
70
 
71
71
  await user.click(screen.getByRole('button', { name: 'Set Project Directory' }));
72
72
  await user.click(screen.getByText('Clear Project Directory'));
@@ -3,7 +3,12 @@ import { ChevronDown, FolderOpen, FolderX, Pencil } from 'lucide-react';
3
3
  import { useChatSessionProject } from '@/features/chat/features/session/hooks/use-chat-session-project';
4
4
  import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
5
5
  import { ChatSessionProjectDialog } from './chat-session-project-dialog';
6
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ createPopoverAvailableHeightLimit,
11
+ } from '@/shared/components/ui/popover';
7
12
  import { t } from '@/shared/lib/i18n';
8
13
 
9
14
  type ChatSessionProjectBadgeProps = {
@@ -13,6 +18,10 @@ type ChatSessionProjectBadgeProps = {
13
18
  persistToServer: boolean;
14
19
  };
15
20
 
21
+ const SESSION_PROJECT_MENU_STYLE = {
22
+ maxHeight: createPopoverAvailableHeightLimit('18rem'),
23
+ };
24
+
16
25
  export function ChatSessionProjectBadge({
17
26
  sessionKey,
18
27
  projectName,
@@ -57,7 +66,11 @@ export function ChatSessionProjectBadge({
57
66
  </span>
58
67
  </button>
59
68
  </PopoverTrigger>
60
- <PopoverContent align="start" className="w-72 p-2">
69
+ <PopoverContent
70
+ align="start"
71
+ className="w-72 p-2"
72
+ style={SESSION_PROJECT_MENU_STYLE}
73
+ >
61
74
  <div className="px-3 pb-2 pt-1">
62
75
  <div className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
63
76
  {projectName}
@@ -0,0 +1,24 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatSidebarCreateMenu } from "@/features/chat/features/session-type/components/chat-sidebar-create-menu";
4
+ import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
5
+
6
+ const options = [
7
+ { value: "native", label: "Native", ready: true },
8
+ { value: "codex", label: "Codex", ready: true },
9
+ ];
10
+
11
+ describe("ChatSidebarCreateMenu", () => {
12
+ it("keeps the session type menu height bounded", () => {
13
+ render(<ChatSidebarCreateMenu options={options} onSelect={vi.fn()} />);
14
+
15
+ const createMenu = screen.getByText("Session Type").parentElement;
16
+ expect(createMenu?.style.maxHeight).toBe(
17
+ createPopoverAvailableHeightLimit("18rem"),
18
+ );
19
+ expect(createMenu?.style.maxHeight).toContain("max(0px");
20
+ expect(createMenu?.style.maxHeight).toContain("100vh");
21
+ expect(createMenu?.style.maxHeight).toContain("2rem");
22
+ expect(createMenu?.className).toContain("overflow-y-auto");
23
+ });
24
+ });
@@ -1,9 +1,15 @@
1
1
  import { ChatSessionTypeOptionItem } from "@/features/chat/features/session-type/components/chat-session-type-option-item";
2
2
  import type { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store";
3
+ import { createPopoverAvailableHeightLimit } from "@/shared/components/ui/popover";
3
4
  import { t } from "@/shared/lib/i18n";
4
5
  import { cn } from "@/shared/lib/utils";
5
6
 
6
7
  type SessionTypeOption = ChatInputSnapshot["sessionTypeOptions"][number];
8
+ const CHAT_SIDEBAR_CREATE_MENU_MAX_HEIGHT =
9
+ createPopoverAvailableHeightLimit("18rem");
10
+ const CHAT_SIDEBAR_CREATE_MENU_STYLE = {
11
+ maxHeight: CHAT_SIDEBAR_CREATE_MENU_MAX_HEIGHT,
12
+ };
7
13
 
8
14
  type ChatSidebarCreateMenuProps = {
9
15
  options: readonly SessionTypeOption[];
@@ -21,7 +27,10 @@ export function ChatSidebarCreateMenu({
21
27
  titleClassName,
22
28
  }: ChatSidebarCreateMenuProps) {
23
29
  return (
24
- <div className={cn("space-y-1", className)}>
30
+ <div
31
+ className={cn("space-y-1 overflow-y-auto overscroll-contain", className)}
32
+ style={CHAT_SIDEBAR_CREATE_MENU_STYLE}
33
+ >
25
34
  <div
26
35
  className={cn(
27
36
  "px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400",
@@ -0,0 +1,193 @@
1
+ import type { ReactNode } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { ChatConversationWelcome } from "@/features/chat/features/welcome/components/chat-conversation-welcome";
6
+ import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
7
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
8
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
10
+ import type { ChatQuerySnapshot } from "@/features/chat/stores/ncp-chat-query.store";
11
+
12
+ const mocks = vi.hoisted(() => ({
13
+ setSelectedAgentId: vi.fn(),
14
+ applyPromptSuggestion: vi.fn(),
15
+ setPendingProjectRoot: vi.fn(),
16
+ setPendingSessionType: vi.fn(),
17
+ agents: [
18
+ { id: "main", displayName: "Main", runtime: "native" },
19
+ { id: "engineer", displayName: "Engineer", runtime: "codex" },
20
+ ],
21
+ }));
22
+
23
+ vi.mock("@/features/chat/features/welcome/components/chat-welcome", () => ({
24
+ ChatWelcome: ({
25
+ inputSlot,
26
+ onSelectAgent,
27
+ onSelectPrompt,
28
+ onSelectProjectRoot,
29
+ onSelectSessionType,
30
+ }: {
31
+ inputSlot: ReactNode;
32
+ onSelectAgent: (agentId: string) => void;
33
+ onSelectPrompt: (prompt: string) => void;
34
+ onSelectProjectRoot: (projectRoot: string) => void;
35
+ onSelectSessionType: (sessionType: string) => void;
36
+ }) => (
37
+ <div>
38
+ {inputSlot}
39
+ <button type="button" onClick={() => onSelectAgent("engineer")}>
40
+ switch draft agent
41
+ </button>
42
+ <button type="button" onClick={() => onSelectPrompt("example prompt")}>
43
+ pick prompt
44
+ </button>
45
+ <button type="button" onClick={() => onSelectSessionType("codex")}>
46
+ switch session type
47
+ </button>
48
+ <button
49
+ type="button"
50
+ onClick={() => onSelectProjectRoot("/tmp/project-alpha")}
51
+ >
52
+ select draft project
53
+ </button>
54
+ </div>
55
+ ),
56
+ }));
57
+
58
+ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
59
+ usePresenter: () => ({
60
+ chatSessionListManager: {
61
+ setSelectedAgentId: mocks.setSelectedAgentId,
62
+ },
63
+ chatInputManager: {
64
+ applyPromptSuggestion: mocks.applyPromptSuggestion,
65
+ setPendingProjectRoot: mocks.setPendingProjectRoot,
66
+ setPendingSessionType: mocks.setPendingSessionType,
67
+ },
68
+ }),
69
+ }));
70
+
71
+ vi.mock("@/shared/hooks/use-agents", () => ({
72
+ useAgents: () => ({
73
+ data: { agents: mocks.agents },
74
+ }),
75
+ }));
76
+
77
+ function createFetchedQuery<TData>(data: TData) {
78
+ return {
79
+ data,
80
+ error: null,
81
+ fetchStatus: "idle",
82
+ isFetched: true,
83
+ isFetching: false,
84
+ isLoading: false,
85
+ isSuccess: true,
86
+ status: "success",
87
+ };
88
+ }
89
+
90
+ function resetWelcomeTestState() {
91
+ mocks.setSelectedAgentId.mockReset();
92
+ mocks.applyPromptSuggestion.mockReset();
93
+ mocks.setPendingProjectRoot.mockReset();
94
+ mocks.setPendingSessionType.mockReset();
95
+ useChatInputStore.setState({
96
+ snapshot: {
97
+ ...useChatInputStore.getState().snapshot,
98
+ defaultSessionType: "native",
99
+ pendingSessionType: "native",
100
+ selectedSessionType: undefined,
101
+ pendingProjectRoot: null,
102
+ },
103
+ });
104
+ useChatSessionListStore.setState({
105
+ snapshot: {
106
+ ...useChatSessionListStore.getState().snapshot,
107
+ selectedAgentId: "main",
108
+ },
109
+ });
110
+ useChatThreadStore.setState({
111
+ snapshot: {
112
+ ...useChatThreadStore.getState().snapshot,
113
+ agentId: null,
114
+ },
115
+ });
116
+ useChatQueryStore.setState({
117
+ snapshot: {
118
+ configQuery: createFetchedQuery({
119
+ agents: {
120
+ defaults: {
121
+ workspace: "/Users/demo/.nextclaw/workspace",
122
+ },
123
+ },
124
+ providers: {},
125
+ }) as unknown as ChatQuerySnapshot["configQuery"],
126
+ sessionTypesQuery: createFetchedQuery({
127
+ defaultType: "native",
128
+ options: [
129
+ { value: "native", label: "Native", ready: true },
130
+ { value: "codex", label: "Codex", ready: true },
131
+ ],
132
+ }) as unknown as ChatQuerySnapshot["sessionTypesQuery"],
133
+ sessionsQuery: createFetchedQuery({
134
+ sessions: [],
135
+ total: 0,
136
+ }) as unknown as ChatQuerySnapshot["sessionsQuery"],
137
+ },
138
+ });
139
+ }
140
+
141
+ describe("ChatConversationWelcome", () => {
142
+ beforeEach(resetWelcomeTestState);
143
+
144
+ it("stores the selected welcome project through the input manager", async () => {
145
+ const user = userEvent.setup();
146
+
147
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
148
+
149
+ await user.click(
150
+ screen.getByRole("button", { name: "select draft project" }),
151
+ );
152
+
153
+ expect(mocks.setPendingProjectRoot).toHaveBeenCalledWith("/tmp/project-alpha");
154
+ });
155
+
156
+ it("applies a selected prompt suggestion through the input manager", async () => {
157
+ const user = userEvent.setup();
158
+
159
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
160
+
161
+ await user.click(
162
+ screen.getByRole("button", { name: "pick prompt" }),
163
+ );
164
+
165
+ expect(mocks.applyPromptSuggestion).toHaveBeenCalledWith("example prompt");
166
+ });
167
+
168
+
169
+ it("syncs the pending session type when switching the draft agent", async () => {
170
+ const user = userEvent.setup();
171
+
172
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
173
+
174
+ await user.click(
175
+ screen.getByRole("button", { name: "switch draft agent" }),
176
+ );
177
+
178
+ expect(mocks.setSelectedAgentId).toHaveBeenCalledWith("engineer");
179
+ expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
180
+ });
181
+
182
+ it("stores an explicit welcome session type selection", async () => {
183
+ const user = userEvent.setup();
184
+
185
+ render(<ChatConversationWelcome inputSlot={<div data-testid="input-slot" />} />);
186
+
187
+ await user.click(
188
+ screen.getByRole("button", { name: "switch session type" }),
189
+ );
190
+
191
+ expect(mocks.setPendingSessionType).toHaveBeenCalledWith("codex");
192
+ });
193
+ });