@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
@@ -150,17 +150,18 @@ export function resolveToolCardStatus(params: {
150
150
  ChatToolPartViewModel,
151
151
  "kind" | "hasResult" | "statusTone" | "statusLabel"
152
152
  > {
153
+ const { cancelled, error, result, status, texts } = params;
153
154
  const rawStatus =
154
- typeof params.status === "string" ? params.status.trim().toLowerCase() : "";
155
+ typeof status === "string" ? status.trim().toLowerCase() : "";
155
156
  const hasError =
156
- typeof params.error === "string" && params.error.trim().length > 0;
157
- const isCancelled = params.cancelled === true || rawStatus === "cancelled";
157
+ typeof error === "string" && error.trim().length > 0;
158
+ const isCancelled = cancelled === true || rawStatus === "cancelled";
158
159
  if (isCancelled) {
159
160
  return {
160
161
  kind: "result",
161
162
  hasResult: true,
162
163
  statusTone: "cancelled",
163
- statusLabel: params.texts.toolStatusCancelledLabel,
164
+ statusLabel: texts.toolStatusCancelledLabel,
164
165
  };
165
166
  }
166
167
  if (hasError || rawStatus === "error") {
@@ -168,15 +169,15 @@ export function resolveToolCardStatus(params: {
168
169
  kind: "result",
169
170
  hasResult: true,
170
171
  statusTone: "error",
171
- statusLabel: params.texts.toolStatusFailedLabel,
172
+ statusLabel: texts.toolStatusFailedLabel,
172
173
  };
173
174
  }
174
- if (rawStatus === "result" || params.result != null) {
175
+ if (rawStatus === "result" || result != null) {
175
176
  return {
176
177
  kind: "result",
177
178
  hasResult: true,
178
179
  statusTone: "success",
179
- statusLabel: params.texts.toolStatusCompletedLabel,
180
+ statusLabel: texts.toolStatusCompletedLabel,
180
181
  };
181
182
  }
182
183
  if (rawStatus === "partial-call") {
@@ -184,14 +185,14 @@ export function resolveToolCardStatus(params: {
184
185
  kind: "call",
185
186
  hasResult: false,
186
187
  statusTone: "running",
187
- statusLabel: params.texts.toolStatusRunningLabel,
188
+ statusLabel: texts.toolStatusRunningLabel,
188
189
  };
189
190
  }
190
191
  return {
191
192
  kind: "call",
192
193
  hasResult: false,
193
194
  statusTone: "running",
194
- statusLabel: params.texts.toolStatusRunningLabel,
195
+ statusLabel: texts.toolStatusRunningLabel,
195
196
  };
196
197
  }
197
198
 
@@ -24,16 +24,17 @@ export function buildRawPreviewBlock(params: {
24
24
  oldStartLine?: number | null;
25
25
  newStartLine?: number | null;
26
26
  }): ParsedBlock | null {
27
- const previewText = params.text.trim();
27
+ const { newStartLine: rawNewStartLine, oldStartLine: rawOldStartLine, operation, path, text } = params;
28
+ const previewText = text.trim();
28
29
  if (!previewText) {
29
30
  return null;
30
31
  }
31
32
  const previewKind =
32
- params.operation?.trim().toLowerCase() === "write" ? "add" : "context";
33
+ operation?.trim().toLowerCase() === "write" ? "add" : "context";
33
34
  const oldStartLine =
34
- typeof params.oldStartLine === "number" ? params.oldStartLine : 1;
35
+ typeof rawOldStartLine === "number" ? rawOldStartLine : 1;
35
36
  const newStartLine =
36
- typeof params.newStartLine === "number" ? params.newStartLine : 1;
37
+ typeof rawNewStartLine === "number" ? rawNewStartLine : 1;
37
38
  const lines = buildPreviewLines({
38
39
  text: previewText,
39
40
  kind: previewKind,
@@ -41,10 +42,10 @@ export function buildRawPreviewBlock(params: {
41
42
  newStartLine,
42
43
  });
43
44
  return {
44
- path: params.path,
45
+ path,
45
46
  display: "preview",
46
47
  caption: buildCaption({
47
- operation: params.operation,
48
+ operation,
48
49
  lines,
49
50
  }),
50
51
  lines,
@@ -62,10 +63,11 @@ export function buildFullReplaceBlock(params: {
62
63
  oldStartLine?: number | null;
63
64
  newStartLine?: number | null;
64
65
  }): ParsedBlock | null {
66
+ const { afterText, beforeText, operation, path } = params;
65
67
  const { oldStartLine, newStartLine } = readDefaultDiffStartLines(params);
66
68
  const lines = buildLineDiff({
67
- beforeText: params.beforeText ?? "",
68
- afterText: params.afterText ?? "",
69
+ beforeText: beforeText ?? "",
70
+ afterText: afterText ?? "",
69
71
  oldStartLine,
70
72
  newStartLine,
71
73
  });
@@ -74,16 +76,16 @@ export function buildFullReplaceBlock(params: {
74
76
  return null;
75
77
  }
76
78
  return {
77
- path: params.path,
79
+ path,
78
80
  display: "diff",
79
81
  caption: buildCaption({
80
- operation: params.operation,
82
+ operation,
81
83
  lines,
82
84
  }),
83
85
  lines: limited.lines,
84
86
  ...(limited.truncated ? { fullLines: lines } : {}),
85
- ...(params.beforeText != null ? { beforeText: params.beforeText } : {}),
86
- ...(params.afterText != null ? { afterText: params.afterText } : {}),
87
+ ...(beforeText != null ? { beforeText } : {}),
88
+ ...(afterText != null ? { afterText } : {}),
87
89
  ...(typeof oldStartLine === "number" ? { oldStartLine } : {}),
88
90
  ...(typeof newStartLine === "number" ? { newStartLine } : {}),
89
91
  truncated: limited.truncated,
@@ -95,23 +97,24 @@ function updateApplyPatchCursor(params: {
95
97
  flushCurrent: () => void;
96
98
  setCurrent: (path: string, operation: string) => void;
97
99
  }): boolean {
98
- if (params.line.startsWith("*** Update File: ")) {
99
- params.flushCurrent();
100
- params.setCurrent(
101
- params.line.slice("*** Update File: ".length).trim(),
100
+ const { flushCurrent, line, setCurrent } = params;
101
+ if (line.startsWith("*** Update File: ")) {
102
+ flushCurrent();
103
+ setCurrent(
104
+ line.slice("*** Update File: ".length).trim(),
102
105
  "update",
103
106
  );
104
107
  return true;
105
108
  }
106
- if (params.line.startsWith("*** Add File: ")) {
107
- params.flushCurrent();
108
- params.setCurrent(params.line.slice("*** Add File: ".length).trim(), "add");
109
+ if (line.startsWith("*** Add File: ")) {
110
+ flushCurrent();
111
+ setCurrent(line.slice("*** Add File: ".length).trim(), "add");
109
112
  return true;
110
113
  }
111
- if (params.line.startsWith("*** Delete File: ")) {
112
- params.flushCurrent();
113
- params.setCurrent(
114
- params.line.slice("*** Delete File: ".length).trim(),
114
+ if (line.startsWith("*** Delete File: ")) {
115
+ flushCurrent();
116
+ setCurrent(
117
+ line.slice("*** Delete File: ".length).trim(),
115
118
  "delete",
116
119
  );
117
120
  return true;
@@ -128,18 +131,18 @@ function appendPatchLine(params: {
128
131
  oldLineNumber?: number;
129
132
  newLineNumber?: number;
130
133
  } {
131
- const { currentLines, line } = params;
134
+ const { currentLines, line, newLineNumber, oldLineNumber } = params;
132
135
  if (line.startsWith("+")) {
133
136
  currentLines.push(
134
137
  createLine({
135
138
  kind: "add",
136
139
  text: line.slice(1),
137
- newLineNumber: params.newLineNumber,
140
+ newLineNumber,
138
141
  }),
139
142
  );
140
143
  return {
141
- oldLineNumber: params.oldLineNumber,
142
- newLineNumber: incrementLineNumber(params.newLineNumber),
144
+ oldLineNumber,
145
+ newLineNumber: incrementLineNumber(newLineNumber),
143
146
  };
144
147
  }
145
148
  if (line.startsWith("-")) {
@@ -147,12 +150,12 @@ function appendPatchLine(params: {
147
150
  createLine({
148
151
  kind: "remove",
149
152
  text: line.slice(1),
150
- oldLineNumber: params.oldLineNumber,
153
+ oldLineNumber,
151
154
  }),
152
155
  );
153
156
  return {
154
- oldLineNumber: incrementLineNumber(params.oldLineNumber),
155
- newLineNumber: params.newLineNumber,
157
+ oldLineNumber: incrementLineNumber(oldLineNumber),
158
+ newLineNumber,
156
159
  };
157
160
  }
158
161
  if (line.startsWith(" ")) {
@@ -160,18 +163,18 @@ function appendPatchLine(params: {
160
163
  createLine({
161
164
  kind: "context",
162
165
  text: line.slice(1),
163
- oldLineNumber: params.oldLineNumber,
164
- newLineNumber: params.newLineNumber,
166
+ oldLineNumber,
167
+ newLineNumber,
165
168
  }),
166
169
  );
167
170
  return {
168
- oldLineNumber: incrementLineNumber(params.oldLineNumber),
169
- newLineNumber: incrementLineNumber(params.newLineNumber),
171
+ oldLineNumber: incrementLineNumber(oldLineNumber),
172
+ newLineNumber: incrementLineNumber(newLineNumber),
170
173
  };
171
174
  }
172
175
  return {
173
- oldLineNumber: params.oldLineNumber,
174
- newLineNumber: params.newLineNumber,
176
+ oldLineNumber,
177
+ newLineNumber,
175
178
  };
176
179
  }
177
180
 
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import { useMemo, useState, type ReactNode } from 'react';
2
2
  import { Plus } from 'lucide-react';
3
- import { ChatSidebarCreateMenu } from "@/features/chat/features/session-type/components/chat-sidebar-create-menu";
3
+ import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
5
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
6
6
  import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
@@ -77,8 +77,9 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
77
77
  align="end"
78
78
  className="w-56 rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
79
79
  >
80
- <ChatSidebarCreateMenu
80
+ <ChatSessionTypeMenu
81
81
  options={sessionTypeOptions}
82
+ selectedSessionType={preferredSessionType}
82
83
  onSelect={(sessionType) => {
83
84
  onCreateSession(sessionType, group.projectRoot);
84
85
  setOpenProjectRoot(null);
@@ -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 { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-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("ChatSessionTypeMenu", () => {
12
+ it("keeps the session type menu height bounded", () => {
13
+ render(<ChatSessionTypeMenu 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
+ });