@nextclaw/ui 0.14.1-beta.0 → 0.14.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/assets/{api-C6EDHBDR.js → api-dQ6uQSZG.js} +3 -3
- package/dist/assets/{channels-list-page-DKj6m1Hc.js → channels-list-page-DfpjB8XB.js} +2 -2
- package/dist/assets/chat-page-D3QaEqjs.js +105 -0
- package/dist/assets/{config-split-page-CjlmJy8X.js → config-split-page-CeFfbaqc.js} +1 -1
- package/dist/assets/{confirm-dialog-B39X0pYp.js → confirm-dialog-C95Y4QPC.js} +1 -1
- package/dist/assets/{desktop-update-config-DfUQ8nly.js → desktop-update-config-CcK6jiEb.js} +1 -1
- package/dist/assets/{dist-CmMro3nV.js → dist-DTGzeil6.js} +1 -1
- package/dist/assets/{dist-BIYV2-Ia.js → dist-fGogBmPA.js} +1 -1
- package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
- package/dist/assets/{doc-browser-CDtUtcim.js → doc-browser-DVHhEb96.js} +1 -1
- package/dist/assets/{doc-browser-context-DZbC9zVB.js → doc-browser-context-BfMXwnrz.js} +1 -1
- package/dist/assets/{ellipsis-DDpgTzgx.js → ellipsis-D2F4KMiU.js} +1 -1
- package/dist/assets/{external-link-D2cBOCqn.js → external-link-CcgtBK26.js} +1 -1
- package/dist/assets/index-ByJCbQqr.css +1 -0
- package/dist/assets/index-DjXqiloY.js +103 -0
- package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
- package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
- package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
- package/dist/assets/plus-1Rxl5FlV.js +1 -0
- package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
- package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
- package/dist/assets/{react-DYQXXGbH.js → react-CTqdhFMk.js} +1 -1
- package/dist/assets/remote-fQ7N_r8u.js +1 -0
- package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
- package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
- package/dist/assets/{save-BNVTgBVn.js → save-C9fKWfVB.js} +1 -1
- package/dist/assets/{search-CKCzwLId.js → search-D5FQ5q0X.js} +1 -1
- package/dist/assets/{search-config-page-DG5Q2hF1.js → search-config-page-AMEJJNIS.js} +1 -1
- package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
- package/dist/assets/{select-CIRwTNP4.js → select-Cu5n0E-3.js} +2 -2
- package/dist/assets/{tag-chip-DKhVFtTE.js → tag-chip-C5aOtjxu.js} +1 -1
- package/dist/assets/use-config-D8TESeo6.js +1 -0
- package/dist/assets/x-1qETdQ70.js +1 -0
- package/dist/index.html +14 -14
- package/package.json +9 -9
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +12 -63
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +84 -6
- package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +2 -0
- package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
- package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +64 -24
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +21 -12
- package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -25
- package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
- package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +10 -9
- package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
- package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
- package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
- package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +65 -0
- package/src/features/chat/managers/chat-input.manager.ts +24 -2
- package/src/features/chat/stores/chat-input.store.ts +2 -0
- package/src/features/chat/types/chat-input-bar.types.ts +5 -0
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
- package/src/features/settings/components/config/provider-form.tsx +284 -387
- package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
- package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
- package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
- package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
- package/src/features/settings/pages/secrets-config-page.tsx +2 -1
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
- package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
- package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
- package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
- package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
- package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
- package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
- package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
- package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
- package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
- package/src/features/system-status/components/runtime-security-card.tsx +79 -25
- package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
- package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
- package/src/shared/components/common/searchable-model-input.tsx +102 -87
- package/src/shared/components/ui/README.md +7 -0
- package/src/shared/components/ui/popover.tsx +19 -3
- package/src/shared/components/ui/select.tsx +14 -4
- package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
- package/src/shared/lib/api/index.ts +2 -0
- package/src/shared/lib/api/preferences/preference.types.ts +22 -0
- package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +16 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -1
- package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
- package/dist/assets/chat-page-BXngLJY6.js +0 -105
- package/dist/assets/doc-browser-B6Wc_TIY.js +0 -1
- package/dist/assets/index-DTb_XVIH.js +0 -103
- package/dist/assets/index-ayiPYhf1.css +0 -1
- package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +0 -1
- package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +0 -40
- package/dist/assets/model-config-page-Bcb35AVv.js +0 -1
- package/dist/assets/plus-CYE1onU2.js +0 -1
- package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +0 -1
- package/dist/assets/providers-config-page-CQSibSwO.js +0 -1
- package/dist/assets/remote-BlJDbXOI.js +0 -1
- package/dist/assets/rotate-cw-BWbvFDAX.js +0 -1
- package/dist/assets/runtime-config-page-Tn2yLQ0c.js +0 -1
- package/dist/assets/secrets-config-page-BXn5Uuwp.js +0 -3
- package/dist/assets/use-config-DgKM9VIU.js +0 -1
- package/dist/assets/x-Cm3JjYut.js +0 -1
- package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
- package/src/features/chat/components/chat-welcome.tsx +0 -102
|
@@ -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
|
|
24
|
-
text
|
|
25
|
-
...(typeof
|
|
26
|
-
? { oldLineNumber
|
|
24
|
+
kind,
|
|
25
|
+
text,
|
|
26
|
+
...(typeof oldLineNumber === "number"
|
|
27
|
+
? { oldLineNumber }
|
|
27
28
|
: {}),
|
|
28
|
-
...(typeof
|
|
29
|
-
? { 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
|
-
|
|
55
|
-
|
|
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:
|
|
61
|
+
newLineNumber: newStartLine + index,
|
|
60
62
|
})
|
|
61
63
|
: createLine({
|
|
62
64
|
kind: "context",
|
|
63
65
|
text: line,
|
|
64
|
-
oldLineNumber:
|
|
65
|
-
newLineNumber:
|
|
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
|
-
|
|
77
|
-
let
|
|
78
|
+
const { afterLines, beforeLines, newStartLine, oldStartLine } = params;
|
|
79
|
+
let oldLineNumber = oldStartLine;
|
|
80
|
+
let newLineNumber = newStartLine;
|
|
78
81
|
return [
|
|
79
|
-
...
|
|
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
|
-
...
|
|
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:
|
|
106
|
-
() => Array.from({ length:
|
|
109
|
+
{ length: beforeLines.length + 1 },
|
|
110
|
+
() => Array.from({ length: afterLines.length + 1 }, () => 0),
|
|
107
111
|
);
|
|
108
112
|
for (
|
|
109
|
-
let beforeIndex =
|
|
113
|
+
let beforeIndex = beforeLines.length - 1;
|
|
110
114
|
beforeIndex >= 0;
|
|
111
115
|
beforeIndex -= 1
|
|
112
116
|
) {
|
|
113
117
|
for (
|
|
114
|
-
let afterIndex =
|
|
118
|
+
let afterIndex = afterLines.length - 1;
|
|
115
119
|
afterIndex >= 0;
|
|
116
120
|
afterIndex -= 1
|
|
117
121
|
) {
|
|
118
122
|
matrix[beforeIndex]![afterIndex] =
|
|
119
|
-
|
|
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 =
|
|
141
|
-
index <
|
|
147
|
+
let index = beforeIndex;
|
|
148
|
+
index < beforeLines.length;
|
|
142
149
|
index += 1
|
|
143
150
|
) {
|
|
144
|
-
|
|
151
|
+
lines.push(
|
|
145
152
|
createLine({
|
|
146
153
|
kind: "remove",
|
|
147
|
-
text:
|
|
148
|
-
oldLineNumber
|
|
154
|
+
text: beforeLines[index] ?? "",
|
|
155
|
+
oldLineNumber,
|
|
149
156
|
}),
|
|
150
157
|
);
|
|
151
|
-
|
|
158
|
+
oldLineNumber = incrementLineNumber(oldLineNumber);
|
|
152
159
|
}
|
|
153
160
|
for (
|
|
154
|
-
let index =
|
|
155
|
-
index <
|
|
161
|
+
let index = afterIndex;
|
|
162
|
+
index < afterLines.length;
|
|
156
163
|
index += 1
|
|
157
164
|
) {
|
|
158
|
-
|
|
165
|
+
lines.push(
|
|
159
166
|
createLine({
|
|
160
167
|
kind: "add",
|
|
161
|
-
text:
|
|
162
|
-
newLineNumber
|
|
168
|
+
text: afterLines[index] ?? "",
|
|
169
|
+
newLineNumber,
|
|
163
170
|
}),
|
|
164
171
|
);
|
|
165
|
-
|
|
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
|
|
176
|
-
const
|
|
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
|
|
182
|
-
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 =
|
|
194
|
-
let newLineNumber =
|
|
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
|
|
24
|
-
const
|
|
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 =
|
|
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
|
|
50
|
+
const { afterText, beforeText, newStartLine: rawNewStartLine, oldStartLine: rawOldStartLine, operation } = params;
|
|
51
|
+
const normalizedOperation = operation?.trim().toLowerCase() ?? "";
|
|
50
52
|
const oldStartLine =
|
|
51
|
-
typeof
|
|
52
|
-
?
|
|
53
|
+
typeof rawOldStartLine === "number"
|
|
54
|
+
? rawOldStartLine
|
|
53
55
|
: (normalizedOperation === "delete" ||
|
|
54
56
|
normalizedOperation === "remove") &&
|
|
55
|
-
|
|
57
|
+
beforeText != null
|
|
56
58
|
? 1
|
|
57
59
|
: undefined;
|
|
58
60
|
const newStartLine =
|
|
59
|
-
typeof
|
|
60
|
-
?
|
|
61
|
+
typeof rawNewStartLine === "number"
|
|
62
|
+
? rawNewStartLine
|
|
61
63
|
: (normalizedOperation === "write" || normalizedOperation === "add") &&
|
|
62
|
-
|
|
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
|
|
88
|
+
const { lines, operation, path } = params;
|
|
89
|
+
const limited = limitLines(lines);
|
|
87
90
|
return {
|
|
88
|
-
path
|
|
91
|
+
path,
|
|
89
92
|
display: "diff",
|
|
90
93
|
caption: buildCaption({
|
|
91
|
-
operation
|
|
92
|
-
lines
|
|
94
|
+
operation,
|
|
95
|
+
lines,
|
|
93
96
|
}),
|
|
94
97
|
lines: limited.lines,
|
|
95
|
-
...(limited.truncated ? { fullLines:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'));
|
package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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",
|
package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx
ADDED
|
@@ -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
|
+
});
|