@nextclaw/ui 0.11.21 → 0.11.22
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 +14 -0
- package/dist/assets/{ChannelsList-ByHWHkQS.js → ChannelsList-Zeys_w43.js} +6 -6
- package/dist/assets/ChatPage-DWOU_8P6.js +43 -0
- package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
- package/dist/assets/{DocBrowser-3y_NHZ71.js → DocBrowser-BmtBLFU0.js} +1 -1
- package/dist/assets/{DocBrowserContext-CVJuwCcw.js → DocBrowserContext-YIKkPb76.js} +1 -1
- package/dist/assets/{LogoBadge-D8fyilO-.js → LogoBadge-F7ZWdxLT.js} +1 -1
- package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
- package/dist/assets/{MarketplacePage-CmhsZXr1.js → MarketplacePage-Cd4faegU.js} +2 -2
- package/dist/assets/{McpMarketplacePage-C7PkCYbp.js → McpMarketplacePage-C09Ngs7O.js} +2 -2
- package/dist/assets/ModelConfig-DJgdcgvQ.js +1 -0
- package/dist/assets/ProvidersList-w0rVFIBf.js +1 -0
- package/dist/assets/RemoteAccessPage-BJ_ckkOV.js +1 -0
- package/dist/assets/RuntimeConfig-Cmn2xPQO.js +1 -0
- package/dist/assets/{SearchConfig-Dm7r2yfp.js → SearchConfig-BT13qpR_.js} +1 -1
- package/dist/assets/{SecretsConfig-BBP_mbQh.js → SecretsConfig-CvqEVn0B.js} +2 -2
- package/dist/assets/{SessionsConfig-6wNJloZN.js → SessionsConfig-DHHcYznk.js} +2 -2
- package/dist/assets/{book-open-B26jGBjY.js → book-open-CXoF5nQC.js} +1 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-B-4B29RN.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
- package/dist/assets/{config-BaC29Qf-.js → config-DJswxxE8.js} +1 -1
- package/dist/assets/{createLucideIcon-DiFAvXmK.js → createLucideIcon-CjGHOWb6.js} +1 -1
- package/dist/assets/{dist-pCfWPG1A.js → dist-Cl2QB-2y.js} +1 -1
- package/dist/assets/{dist-kW_O3kyZ.js → dist-nqTTbVdA.js} +1 -1
- package/dist/assets/{external-link-D5-p-Gmm.js → external-link-tIO7zING.js} +1 -1
- package/dist/assets/{hash-BlwrSV0q.js → hash-JWUyl1pT.js} +1 -1
- package/dist/assets/i18n-CDHMXlRZ.js +1 -0
- package/dist/assets/index-BlH4-cBw.css +1 -0
- package/dist/assets/{index-DvKS3L9j.js → index-C6d0xmtm.js} +3 -3
- package/dist/assets/{label-RyXfZqkP.js → label-BIpeNu4r.js} +1 -1
- package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
- package/dist/assets/{logos-Bpl8QTgI.js → logos-DThdM9lk.js} +1 -1
- package/dist/assets/{page-layout--S0YBU0W.js → page-layout-D3Xo605Z.js} +1 -1
- package/dist/assets/plus-PHf8q-Ct.js +1 -0
- package/dist/assets/{popover-BEjfbEwy.js → popover-BJRUGA_H.js} +1 -1
- package/dist/assets/provider-models-bz5y28rq.js +1 -0
- package/dist/assets/{react-BuSP2-8B.js → react-7ZHqQtEV.js} +1 -1
- package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
- package/dist/assets/{save-DPPPpD_c.js → save-DJM5RRWW.js} +1 -1
- package/dist/assets/search-C91yH_6y.js +1 -0
- package/dist/assets/{security-config-6t78Ph-I.js → security-config-T5zpg16O.js} +1 -1
- package/dist/assets/{select-CT50pzod.js → select-DSkTc61S.js} +1 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
- package/dist/assets/{status-dot-BbBqRHfh.js → status-dot-LNBlDu3q.js} +1 -1
- package/dist/assets/{switch-D3l6AcCk.js → switch-Bo-Y46HZ.js} +1 -1
- package/dist/assets/tabs-custom-DXv507_2.js +1 -0
- package/dist/assets/{trash-2-B2_AGVE3.js → trash-2-DFZmW6Gg.js} +1 -1
- package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
- package/dist/assets/{useMutation-BzCrO8j-.js → useMutation-DrZrOgVL.js} +1 -1
- package/dist/assets/x-D7Q1yqSF.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +3 -3
- package/src/api/ncp-session.test.ts +37 -0
- package/src/api/ncp-session.ts +29 -1
- package/src/api/server-path.ts +23 -0
- package/src/api/types.ts +41 -0
- package/src/components/chat/ChatConversationPanel.test.tsx +43 -7
- package/src/components/chat/ChatConversationPanel.tsx +23 -17
- package/src/components/chat/ChatSidebar.test.tsx +2 -2
- package/src/components/chat/ChatSidebar.tsx +2 -2
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
- package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -9
- package/src/components/chat/adapters/chat-message.adapter.test.ts +76 -4
- package/src/components/chat/adapters/{chat-message.file-operation-card.ts → file-operation/card.ts} +74 -181
- package/src/components/chat/adapters/{chat-message.file-operation-diff.ts → file-operation/diff.ts} +178 -188
- package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
- package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
- package/src/components/chat/chat-composer-state.ts +3 -3
- package/src/components/chat/chat-session-display.test.ts +21 -0
- package/src/components/chat/chat-session-display.ts +6 -1
- package/src/components/chat/containers/chat-input-bar.container.tsx +21 -24
- package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
- package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
- package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
- package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
- package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
- package/src/components/chat/ncp/NcpChatPage.tsx +55 -17
- package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
- package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +3 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +16 -0
- package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
- package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
- package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
- package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
- package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
- package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
- package/src/components/chat/stores/chat-input.store.ts +6 -3
- package/src/components/chat/stores/chat-thread.store.ts +6 -2
- package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
- package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
- package/src/hooks/server-path/use-server-path-browse.ts +19 -0
- package/src/hooks/useConfig.ts +26 -1
- package/src/lib/i18n/i18n-language-owner.ts +94 -0
- package/src/lib/i18n/i18n.path-picker.ts +12 -0
- package/src/lib/i18n.chat.ts +23 -0
- package/src/lib/i18n.ts +21 -84
- package/src/lib/session-project/session-project.utils.ts +30 -0
- package/dist/assets/ChatPage-FdT3pDnw.js +0 -42
- package/dist/assets/DocBrowser-CMdPdbZj.js +0 -1
- package/dist/assets/MarketplacePage-9oKmxN2n.js +0 -1
- package/dist/assets/ModelConfig-DmCY6jWM.js +0 -1
- package/dist/assets/ProvidersList-ClT-34aX.js +0 -1
- package/dist/assets/RemoteAccessPage-B6hUZl1O.js +0 -1
- package/dist/assets/RuntimeConfig-C5aqliGk.js +0 -1
- package/dist/assets/chat-session-display-Bjmn4aIZ.js +0 -1
- package/dist/assets/i18n-CSytxMFI.js +0 -1
- package/dist/assets/index-CUy6doWo.css +0 -1
- package/dist/assets/loader-circle-B2J777gj.js +0 -1
- package/dist/assets/plus-CM9XJ0Tf.js +0 -1
- package/dist/assets/provider-models-C8JQUd1E.js +0 -1
- package/dist/assets/search-Ctaw34Kp.js +0 -1
- package/dist/assets/skeleton-Bycyb0zU.js +0 -1
- package/dist/assets/tabs-custom-TZQ5WPWP.js +0 -1
- package/dist/assets/useConfirmDialog-BDpdjfIO.js +0 -1
- package/dist/assets/x-CHOBE-63.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
- /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
- /package/dist/assets/{marketplace-localization-CXeGRf6E.js → marketplace-localization-CxSTG9wr.js} +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { ChatFileOperationLineViewModel } from "@nextclaw/agent-chat-ui";
|
|
2
|
+
|
|
3
|
+
const MAX_DIFF_MATRIX_CELLS = 12_000;
|
|
4
|
+
const UNIFIED_DIFF_HUNK_HEADER_PATTERN =
|
|
5
|
+
/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
6
|
+
|
|
7
|
+
export function splitLines(value: string): string[] {
|
|
8
|
+
const normalized = value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
9
|
+
return normalized === "" ? [] : normalized.split("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function incrementLineNumber(value?: number): number | undefined {
|
|
13
|
+
return typeof value === "number" ? value + 1 : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createLine(params: {
|
|
17
|
+
kind: ChatFileOperationLineViewModel["kind"];
|
|
18
|
+
text: string;
|
|
19
|
+
oldLineNumber?: number;
|
|
20
|
+
newLineNumber?: number;
|
|
21
|
+
}): ChatFileOperationLineViewModel {
|
|
22
|
+
return {
|
|
23
|
+
kind: params.kind,
|
|
24
|
+
text: params.text,
|
|
25
|
+
...(typeof params.oldLineNumber === "number"
|
|
26
|
+
? { oldLineNumber: params.oldLineNumber }
|
|
27
|
+
: {}),
|
|
28
|
+
...(typeof params.newLineNumber === "number"
|
|
29
|
+
? { newLineNumber: params.newLineNumber }
|
|
30
|
+
: {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readUnifiedDiffHunkStart(line: string): {
|
|
35
|
+
oldLineNumber: number;
|
|
36
|
+
newLineNumber: number;
|
|
37
|
+
} | null {
|
|
38
|
+
const match = UNIFIED_DIFF_HUNK_HEADER_PATTERN.exec(line);
|
|
39
|
+
if (!match) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
oldLineNumber: Number(match[1]),
|
|
44
|
+
newLineNumber: Number(match[2]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildPreviewLines(params: {
|
|
49
|
+
text: string;
|
|
50
|
+
kind: "add" | "context";
|
|
51
|
+
oldStartLine: number;
|
|
52
|
+
newStartLine: number;
|
|
53
|
+
}): ChatFileOperationLineViewModel[] {
|
|
54
|
+
return splitLines(params.text).map((line, index) =>
|
|
55
|
+
params.kind === "add"
|
|
56
|
+
? createLine({
|
|
57
|
+
kind: "add",
|
|
58
|
+
text: line,
|
|
59
|
+
newLineNumber: params.newStartLine + index,
|
|
60
|
+
})
|
|
61
|
+
: createLine({
|
|
62
|
+
kind: "context",
|
|
63
|
+
text: line,
|
|
64
|
+
oldLineNumber: params.oldStartLine + index,
|
|
65
|
+
newLineNumber: params.newStartLine + index,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildFallbackDiffLines(params: {
|
|
71
|
+
beforeLines: string[];
|
|
72
|
+
afterLines: string[];
|
|
73
|
+
oldStartLine?: number;
|
|
74
|
+
newStartLine?: number;
|
|
75
|
+
}): ChatFileOperationLineViewModel[] {
|
|
76
|
+
let oldLineNumber = params.oldStartLine;
|
|
77
|
+
let newLineNumber = params.newStartLine;
|
|
78
|
+
return [
|
|
79
|
+
...params.beforeLines.map((line) => {
|
|
80
|
+
const nextLine = createLine({
|
|
81
|
+
kind: "remove",
|
|
82
|
+
text: line,
|
|
83
|
+
oldLineNumber,
|
|
84
|
+
});
|
|
85
|
+
oldLineNumber = incrementLineNumber(oldLineNumber);
|
|
86
|
+
return nextLine;
|
|
87
|
+
}),
|
|
88
|
+
...params.afterLines.map((line) => {
|
|
89
|
+
const nextLine = createLine({
|
|
90
|
+
kind: "add",
|
|
91
|
+
text: line,
|
|
92
|
+
newLineNumber,
|
|
93
|
+
});
|
|
94
|
+
newLineNumber = incrementLineNumber(newLineNumber);
|
|
95
|
+
return nextLine;
|
|
96
|
+
}),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildLcsMatrix(params: {
|
|
101
|
+
beforeLines: string[];
|
|
102
|
+
afterLines: string[];
|
|
103
|
+
}): number[][] {
|
|
104
|
+
const matrix: number[][] = Array.from(
|
|
105
|
+
{ length: params.beforeLines.length + 1 },
|
|
106
|
+
() => Array.from({ length: params.afterLines.length + 1 }, () => 0),
|
|
107
|
+
);
|
|
108
|
+
for (
|
|
109
|
+
let beforeIndex = params.beforeLines.length - 1;
|
|
110
|
+
beforeIndex >= 0;
|
|
111
|
+
beforeIndex -= 1
|
|
112
|
+
) {
|
|
113
|
+
for (
|
|
114
|
+
let afterIndex = params.afterLines.length - 1;
|
|
115
|
+
afterIndex >= 0;
|
|
116
|
+
afterIndex -= 1
|
|
117
|
+
) {
|
|
118
|
+
matrix[beforeIndex]![afterIndex] =
|
|
119
|
+
params.beforeLines[beforeIndex] === params.afterLines[afterIndex]
|
|
120
|
+
? (matrix[beforeIndex + 1]![afterIndex + 1] ?? 0) + 1
|
|
121
|
+
: Math.max(
|
|
122
|
+
matrix[beforeIndex + 1]![afterIndex] ?? 0,
|
|
123
|
+
matrix[beforeIndex]![afterIndex + 1] ?? 0,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return matrix;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function appendRemainingDiffLines(params: {
|
|
131
|
+
lines: ChatFileOperationLineViewModel[];
|
|
132
|
+
beforeLines: string[];
|
|
133
|
+
afterLines: string[];
|
|
134
|
+
beforeIndex: number;
|
|
135
|
+
afterIndex: number;
|
|
136
|
+
oldLineNumber?: number;
|
|
137
|
+
newLineNumber?: number;
|
|
138
|
+
}): void {
|
|
139
|
+
for (
|
|
140
|
+
let index = params.beforeIndex;
|
|
141
|
+
index < params.beforeLines.length;
|
|
142
|
+
index += 1
|
|
143
|
+
) {
|
|
144
|
+
params.lines.push(
|
|
145
|
+
createLine({
|
|
146
|
+
kind: "remove",
|
|
147
|
+
text: params.beforeLines[index] ?? "",
|
|
148
|
+
oldLineNumber: params.oldLineNumber,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
params.oldLineNumber = incrementLineNumber(params.oldLineNumber);
|
|
152
|
+
}
|
|
153
|
+
for (
|
|
154
|
+
let index = params.afterIndex;
|
|
155
|
+
index < params.afterLines.length;
|
|
156
|
+
index += 1
|
|
157
|
+
) {
|
|
158
|
+
params.lines.push(
|
|
159
|
+
createLine({
|
|
160
|
+
kind: "add",
|
|
161
|
+
text: params.afterLines[index] ?? "",
|
|
162
|
+
newLineNumber: params.newLineNumber,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
params.newLineNumber = incrementLineNumber(params.newLineNumber);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildLineDiff(params: {
|
|
170
|
+
beforeText: string;
|
|
171
|
+
afterText: string;
|
|
172
|
+
oldStartLine?: number;
|
|
173
|
+
newStartLine?: number;
|
|
174
|
+
}): ChatFileOperationLineViewModel[] {
|
|
175
|
+
const beforeLines = splitLines(params.beforeText);
|
|
176
|
+
const afterLines = splitLines(params.afterText);
|
|
177
|
+
if (beforeLines.length * afterLines.length > MAX_DIFF_MATRIX_CELLS) {
|
|
178
|
+
return buildFallbackDiffLines({
|
|
179
|
+
beforeLines,
|
|
180
|
+
afterLines,
|
|
181
|
+
oldStartLine: params.oldStartLine,
|
|
182
|
+
newStartLine: params.newStartLine,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lcs = buildLcsMatrix({
|
|
187
|
+
beforeLines,
|
|
188
|
+
afterLines,
|
|
189
|
+
});
|
|
190
|
+
const lines: ChatFileOperationLineViewModel[] = [];
|
|
191
|
+
let beforeIndex = 0;
|
|
192
|
+
let afterIndex = 0;
|
|
193
|
+
let oldLineNumber = params.oldStartLine;
|
|
194
|
+
let newLineNumber = params.newStartLine;
|
|
195
|
+
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
|
|
196
|
+
if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
197
|
+
lines.push(
|
|
198
|
+
createLine({
|
|
199
|
+
kind: "context",
|
|
200
|
+
text: beforeLines[beforeIndex] ?? "",
|
|
201
|
+
oldLineNumber,
|
|
202
|
+
newLineNumber,
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
beforeIndex += 1;
|
|
206
|
+
afterIndex += 1;
|
|
207
|
+
oldLineNumber = incrementLineNumber(oldLineNumber);
|
|
208
|
+
newLineNumber = incrementLineNumber(newLineNumber);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
(lcs[beforeIndex + 1]![afterIndex] ?? 0) >=
|
|
214
|
+
(lcs[beforeIndex]![afterIndex + 1] ?? 0)
|
|
215
|
+
) {
|
|
216
|
+
lines.push(
|
|
217
|
+
createLine({
|
|
218
|
+
kind: "remove",
|
|
219
|
+
text: beforeLines[beforeIndex] ?? "",
|
|
220
|
+
oldLineNumber,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
beforeIndex += 1;
|
|
224
|
+
oldLineNumber = incrementLineNumber(oldLineNumber);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
lines.push(
|
|
229
|
+
createLine({
|
|
230
|
+
kind: "add",
|
|
231
|
+
text: afterLines[afterIndex] ?? "",
|
|
232
|
+
newLineNumber,
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
afterIndex += 1;
|
|
236
|
+
newLineNumber = incrementLineNumber(newLineNumber);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
appendRemainingDiffLines({
|
|
240
|
+
lines,
|
|
241
|
+
beforeLines,
|
|
242
|
+
afterLines,
|
|
243
|
+
beforeIndex,
|
|
244
|
+
afterIndex,
|
|
245
|
+
oldLineNumber,
|
|
246
|
+
newLineNumber,
|
|
247
|
+
});
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { readPartialJsonStringField } from "@/components/chat/adapters/chat-message.partial-json";
|
|
2
|
+
|
|
3
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function readNonEmptyString(value: unknown): string | null {
|
|
8
|
+
if (typeof value !== "string") {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizePath(value: unknown): string | null {
|
|
16
|
+
if (typeof value === "string" && value.trim()) {
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readPositiveInteger(value: unknown): number | null {
|
|
23
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
27
|
+
return Number(value.trim());
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readRecordPayload(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): Record<string, unknown> | null {
|
|
35
|
+
if (isRecord(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "string") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed.startsWith("{")) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
47
|
+
return isRecord(parsed) ? parsed : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function readPartialRecordPayload(
|
|
54
|
+
value: unknown,
|
|
55
|
+
): Record<string, unknown> | null {
|
|
56
|
+
if (isRecord(value)) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value !== "string") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed.startsWith("{")) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const path =
|
|
67
|
+
readPartialJsonStringField(trimmed, [
|
|
68
|
+
"path",
|
|
69
|
+
"filePath",
|
|
70
|
+
"file_path",
|
|
71
|
+
"targetPath",
|
|
72
|
+
"target_path",
|
|
73
|
+
"filename",
|
|
74
|
+
"name",
|
|
75
|
+
])?.value ?? null;
|
|
76
|
+
const content =
|
|
77
|
+
readPartialJsonStringField(trimmed, [
|
|
78
|
+
"content",
|
|
79
|
+
"text",
|
|
80
|
+
"afterText",
|
|
81
|
+
"after_text",
|
|
82
|
+
])?.value ?? null;
|
|
83
|
+
const oldText =
|
|
84
|
+
readPartialJsonStringField(trimmed, [
|
|
85
|
+
"oldText",
|
|
86
|
+
"beforeText",
|
|
87
|
+
"before_text",
|
|
88
|
+
])?.value ?? null;
|
|
89
|
+
const newText =
|
|
90
|
+
readPartialJsonStringField(trimmed, ["newText", "afterText", "after_text"])
|
|
91
|
+
?.value ?? null;
|
|
92
|
+
const patch =
|
|
93
|
+
readPartialJsonStringField(trimmed, [
|
|
94
|
+
"patch",
|
|
95
|
+
"diff",
|
|
96
|
+
"unifiedDiff",
|
|
97
|
+
"unified_diff",
|
|
98
|
+
])?.value ?? null;
|
|
99
|
+
|
|
100
|
+
const partialRecord: Record<string, unknown> = {};
|
|
101
|
+
if (path) {
|
|
102
|
+
partialRecord.path = path;
|
|
103
|
+
}
|
|
104
|
+
if (content) {
|
|
105
|
+
partialRecord.content = content;
|
|
106
|
+
}
|
|
107
|
+
if (oldText) {
|
|
108
|
+
partialRecord.oldText = oldText;
|
|
109
|
+
}
|
|
110
|
+
if (newText) {
|
|
111
|
+
partialRecord.newText = newText;
|
|
112
|
+
}
|
|
113
|
+
if (patch) {
|
|
114
|
+
partialRecord.patch = patch;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Object.keys(partialRecord).length > 0 ? partialRecord : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function readPath(record: Record<string, unknown>): string | null {
|
|
121
|
+
return (
|
|
122
|
+
normalizePath(record.path) ??
|
|
123
|
+
normalizePath(record.filePath) ??
|
|
124
|
+
normalizePath(record.file_path) ??
|
|
125
|
+
normalizePath(record.targetPath) ??
|
|
126
|
+
normalizePath(record.target_path) ??
|
|
127
|
+
normalizePath(record.filename) ??
|
|
128
|
+
normalizePath(record.name)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function readOperation(record: Record<string, unknown>): string | null {
|
|
133
|
+
return (
|
|
134
|
+
readNonEmptyString(record.operation) ??
|
|
135
|
+
readNonEmptyString(record.op) ??
|
|
136
|
+
readNonEmptyString(record.action) ??
|
|
137
|
+
readNonEmptyString(record.kind) ??
|
|
138
|
+
readNonEmptyString(record.type) ??
|
|
139
|
+
readNonEmptyString(record.status)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readLineStart(
|
|
144
|
+
record: Record<string, unknown>,
|
|
145
|
+
keys: string[],
|
|
146
|
+
): number | null {
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
const value = readPositiveInteger(record[key]);
|
|
149
|
+
if (value !== null) {
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function readOldStartLine(
|
|
157
|
+
record: Record<string, unknown>,
|
|
158
|
+
): number | null {
|
|
159
|
+
return readLineStart(record, [
|
|
160
|
+
"oldStartLine",
|
|
161
|
+
"old_start_line",
|
|
162
|
+
"startOldLine",
|
|
163
|
+
"start_old_line",
|
|
164
|
+
"oldLineStart",
|
|
165
|
+
"old_line_start",
|
|
166
|
+
"oldLineNumber",
|
|
167
|
+
"old_line_number",
|
|
168
|
+
"lineStart",
|
|
169
|
+
"line_start",
|
|
170
|
+
"startLine",
|
|
171
|
+
"start_line",
|
|
172
|
+
"lineNumber",
|
|
173
|
+
"line_number",
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function readNewStartLine(
|
|
178
|
+
record: Record<string, unknown>,
|
|
179
|
+
): number | null {
|
|
180
|
+
return readLineStart(record, [
|
|
181
|
+
"newStartLine",
|
|
182
|
+
"new_start_line",
|
|
183
|
+
"startNewLine",
|
|
184
|
+
"start_new_line",
|
|
185
|
+
"newLineStart",
|
|
186
|
+
"new_line_start",
|
|
187
|
+
"newLineNumber",
|
|
188
|
+
"new_line_number",
|
|
189
|
+
"lineStart",
|
|
190
|
+
"line_start",
|
|
191
|
+
"startLine",
|
|
192
|
+
"start_line",
|
|
193
|
+
"lineNumber",
|
|
194
|
+
"line_number",
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function readPatchText(record: Record<string, unknown>): string | null {
|
|
199
|
+
return (
|
|
200
|
+
readNonEmptyString(record.patch) ??
|
|
201
|
+
readNonEmptyString(record.diff) ??
|
|
202
|
+
readNonEmptyString(record.unifiedDiff) ??
|
|
203
|
+
readNonEmptyString(record.unified_diff)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function readBeforeText(record: Record<string, unknown>): string | null {
|
|
208
|
+
return (
|
|
209
|
+
readNonEmptyString(record.beforeText) ??
|
|
210
|
+
readNonEmptyString(record.before_text) ??
|
|
211
|
+
readNonEmptyString(record.oldText) ??
|
|
212
|
+
readNonEmptyString(record.old_text) ??
|
|
213
|
+
readNonEmptyString(record.oldContent) ??
|
|
214
|
+
readNonEmptyString(record.old_content) ??
|
|
215
|
+
readNonEmptyString(record.before) ??
|
|
216
|
+
readNonEmptyString(record.previous)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function readAfterText(record: Record<string, unknown>): string | null {
|
|
221
|
+
return (
|
|
222
|
+
readNonEmptyString(record.afterText) ??
|
|
223
|
+
readNonEmptyString(record.after_text) ??
|
|
224
|
+
readNonEmptyString(record.newText) ??
|
|
225
|
+
readNonEmptyString(record.new_text) ??
|
|
226
|
+
readNonEmptyString(record.newContent) ??
|
|
227
|
+
readNonEmptyString(record.new_content) ??
|
|
228
|
+
readNonEmptyString(record.content) ??
|
|
229
|
+
readNonEmptyString(record.text) ??
|
|
230
|
+
readNonEmptyString(record.after) ??
|
|
231
|
+
readNonEmptyString(record.updated)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -55,7 +55,7 @@ export function deriveSelectedAttachmentIdsFromComposer(nodes: ChatComposerNode[
|
|
|
55
55
|
export function syncComposerSkills(
|
|
56
56
|
nodes: ChatComposerNode[],
|
|
57
57
|
nextSkills: string[],
|
|
58
|
-
skillRecords: Array<{
|
|
58
|
+
skillRecords: Array<{ ref: string; name: string }>
|
|
59
59
|
): ChatComposerNode[] {
|
|
60
60
|
const nextSkillSet = new Set(nextSkills);
|
|
61
61
|
const prunedNodes = removeChatComposerTokenNodes(
|
|
@@ -63,14 +63,14 @@ export function syncComposerSkills(
|
|
|
63
63
|
(node) => node.tokenKind === 'skill' && !nextSkillSet.has(node.tokenKey)
|
|
64
64
|
);
|
|
65
65
|
const existingSkills = extractChatComposerTokenKeys(prunedNodes, 'skill');
|
|
66
|
-
const recordMap = new Map(skillRecords.map((record) => [record.
|
|
66
|
+
const recordMap = new Map(skillRecords.map((record) => [record.ref, record]));
|
|
67
67
|
const appendedNodes = nextSkills
|
|
68
68
|
.filter((skill) => !existingSkills.includes(skill))
|
|
69
69
|
.map((skill) =>
|
|
70
70
|
createChatComposerTokenNode({
|
|
71
71
|
tokenKind: 'skill',
|
|
72
72
|
tokenKey: skill,
|
|
73
|
-
label: recordMap.get(skill)?.
|
|
73
|
+
label: recordMap.get(skill)?.name || skill
|
|
74
74
|
})
|
|
75
75
|
);
|
|
76
76
|
|
|
@@ -27,6 +27,27 @@ describe('chat-session-display', () => {
|
|
|
27
27
|
expect(sessionMatchesQuery(createSession({ label: 'VIP Alpha Thread' }), 'alpha')).toBe(true);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it('matches the search query against the project name and path', () => {
|
|
31
|
+
expect(
|
|
32
|
+
sessionMatchesQuery(
|
|
33
|
+
createSession({
|
|
34
|
+
projectRoot: '/Users/demo/workspace/project-apollo',
|
|
35
|
+
projectName: 'project-apollo'
|
|
36
|
+
}),
|
|
37
|
+
'apollo'
|
|
38
|
+
)
|
|
39
|
+
).toBe(true);
|
|
40
|
+
expect(
|
|
41
|
+
sessionMatchesQuery(
|
|
42
|
+
createSession({
|
|
43
|
+
projectRoot: '/Users/demo/workspace/project-apollo',
|
|
44
|
+
projectName: 'project-apollo'
|
|
45
|
+
}),
|
|
46
|
+
'workspace/project'
|
|
47
|
+
)
|
|
48
|
+
).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
30
51
|
it('treats an empty query as a match', () => {
|
|
31
52
|
expect(sessionMatchesQuery(createSession({ label: 'Anything' }), ' ')).toBe(true);
|
|
32
53
|
});
|
|
@@ -18,7 +18,12 @@ export function sessionMatchesQuery(session: SessionEntryView, query: string): b
|
|
|
18
18
|
return true;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
return [
|
|
21
|
+
return [
|
|
22
|
+
session.key,
|
|
23
|
+
sessionDisplayName(session),
|
|
24
|
+
session.projectRoot ?? '',
|
|
25
|
+
session.projectName ?? '',
|
|
26
|
+
]
|
|
22
27
|
.map(normalizeSessionSearchValue)
|
|
23
28
|
.some((value) => value.includes(normalizedQuery));
|
|
24
29
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from '@/components/chat/chat-recent-skills.manager';
|
|
28
28
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
29
29
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
30
|
+
import type { SessionSkillEntryView } from '@/api/types';
|
|
30
31
|
import { t } from '@/lib/i18n';
|
|
31
32
|
import { toast } from 'sonner';
|
|
32
33
|
|
|
@@ -42,19 +43,17 @@ function buildThinkingLabels(): Record<ChatThinkingLevel, string> {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function toSkillRecords(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
descriptionZh?: string;
|
|
50
|
-
origin?: string;
|
|
51
|
-
}>, officialBadgeLabel: string): ChatSkillRecord[] {
|
|
46
|
+
function toSkillRecords(
|
|
47
|
+
snapshotRecords: SessionSkillEntryView[],
|
|
48
|
+
scopeLabels: Record<SessionSkillEntryView['scope'], string>
|
|
49
|
+
): ChatSkillRecord[] {
|
|
52
50
|
return snapshotRecords.map((record) => ({
|
|
53
|
-
key: record.
|
|
54
|
-
label: record.
|
|
51
|
+
key: record.ref,
|
|
52
|
+
label: record.name,
|
|
53
|
+
scopeLabel: scopeLabels[record.scope],
|
|
55
54
|
description: record.description,
|
|
56
55
|
descriptionZh: record.descriptionZh,
|
|
57
|
-
badgeLabel: record.
|
|
56
|
+
badgeLabel: scopeLabels[record.scope]
|
|
58
57
|
}));
|
|
59
58
|
}
|
|
60
59
|
|
|
@@ -88,20 +87,18 @@ export function ChatInputBarContainer() {
|
|
|
88
87
|
const inputBarRef = useRef<ChatInputBarHandle | null>(null);
|
|
89
88
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
90
89
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
const skillScopeLabels = useMemo<Record<'project' | 'workspace', string>>(() => {
|
|
91
|
+
return {
|
|
92
|
+
project: t('chatSkillScopeProject'),
|
|
93
|
+
workspace: t('chatSkillScopeWorkspace'),
|
|
94
|
+
};
|
|
96
95
|
}, [language]);
|
|
97
96
|
const slashTexts = useMemo(
|
|
98
97
|
() => {
|
|
99
|
-
// Keep memo reactive to locale switches even though `t` is imported as a stable function.
|
|
100
|
-
const locale = language;
|
|
101
|
-
void locale;
|
|
102
98
|
return {
|
|
103
99
|
slashSkillSubtitle: t('chatSlashTypeSkill'),
|
|
104
100
|
slashSkillSpecLabel: t('chatSlashSkillSpec'),
|
|
101
|
+
slashSkillScopeLabel: t('chatSlashSkillScope'),
|
|
105
102
|
noSkillDescription: t('chatSkillsPickerNoDescription')
|
|
106
103
|
};
|
|
107
104
|
},
|
|
@@ -109,8 +106,8 @@ export function ChatInputBarContainer() {
|
|
|
109
106
|
);
|
|
110
107
|
|
|
111
108
|
const skillRecords = useMemo(
|
|
112
|
-
() => toSkillRecords(snapshot.skillRecords,
|
|
113
|
-
[snapshot.skillRecords,
|
|
109
|
+
() => toSkillRecords(snapshot.skillRecords, skillScopeLabels),
|
|
110
|
+
[snapshot.skillRecords, skillScopeLabels]
|
|
114
111
|
);
|
|
115
112
|
const modelRecords = useMemo(() => toModelRecords(snapshot.modelOptions), [snapshot.modelOptions]);
|
|
116
113
|
const recentModelValues = chatRecentModelsManager.resolveVisible({
|
|
@@ -137,10 +134,10 @@ export function ChatInputBarContainer() {
|
|
|
137
134
|
: hasModelOptions
|
|
138
135
|
? t('chatInputPlaceholder')
|
|
139
136
|
: t('chatModelNoOptions');
|
|
140
|
-
const recentModelsLabel =
|
|
141
|
-
const allModelsLabel =
|
|
142
|
-
const recentSkillsLabel =
|
|
143
|
-
const allSkillsLabel =
|
|
137
|
+
const recentModelsLabel = t('chatPickerRecentModels');
|
|
138
|
+
const allModelsLabel = t('chatPickerAllModels');
|
|
139
|
+
const recentSkillsLabel = t('chatPickerRecent');
|
|
140
|
+
const allSkillsLabel = t('chatPickerAllSkills');
|
|
144
141
|
|
|
145
142
|
const slashItems = useMemo(
|
|
146
143
|
() => buildChatSlashItems(skillRecords, slashQuery ?? '', slashTexts, recentSkillValues),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { t } from '@/lib/i18n';
|
|
2
|
+
import { useChatSessionUpdate } from '@/components/chat/hooks/use-chat-session-update';
|
|
3
|
+
|
|
4
|
+
type UpdateChatSessionLabelParams = {
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
label: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function useChatSessionLabel() {
|
|
10
|
+
const updateSession = useChatSessionUpdate();
|
|
11
|
+
|
|
12
|
+
return async (params: UpdateChatSessionLabelParams): Promise<void> => {
|
|
13
|
+
await updateSession({
|
|
14
|
+
sessionKey: params.sessionKey,
|
|
15
|
+
patch: { label: params.label },
|
|
16
|
+
successMessage: t('configSavedApplied'),
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
}
|