@nextclaw/ui 0.11.20 → 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 +25 -0
- package/dist/assets/{ChannelsList-DAx7wv0_.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-DKkE3Y4I.js → DocBrowser-BmtBLFU0.js} +1 -1
- package/dist/assets/{DocBrowserContext-BcZRBsCg.js → DocBrowserContext-YIKkPb76.js} +1 -1
- package/dist/assets/{LogoBadge-BIPDLEwK.js → LogoBadge-F7ZWdxLT.js} +1 -1
- package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
- package/dist/assets/{MarketplacePage-Dlp5BgCh.js → MarketplacePage-Cd4faegU.js} +2 -2
- package/dist/assets/{McpMarketplacePage-CwKtAil8.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-v46R5a2U.js → SearchConfig-BT13qpR_.js} +1 -1
- package/dist/assets/{SecretsConfig-CXvUpbB_.js → SecretsConfig-CvqEVn0B.js} +2 -2
- package/dist/assets/{SessionsConfig-7vUHMtOh.js → SessionsConfig-DHHcYznk.js} +2 -2
- package/dist/assets/{book-open-DzSduAaw.js → book-open-CXoF5nQC.js} +1 -1
- package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-C1vpvW4r.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
- package/dist/assets/{config-Df97LeLR.js → config-DJswxxE8.js} +1 -1
- package/dist/assets/{createLucideIcon-CcR5wVoU.js → createLucideIcon-CjGHOWb6.js} +1 -1
- package/dist/assets/{dist-Dii9v3X9.js → dist-Cl2QB-2y.js} +1 -1
- package/dist/assets/{dist-BMlnBah3.js → dist-nqTTbVdA.js} +1 -1
- package/dist/assets/{external-link-CnSDrvJE.js → external-link-tIO7zING.js} +1 -1
- package/dist/assets/{hash-CAnX6PNt.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-B0DzQqwv.js → index-C6d0xmtm.js} +3 -3
- package/dist/assets/{label-CtIFj7_6.js → label-BIpeNu4r.js} +1 -1
- package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
- package/dist/assets/{logos-3KFNiOej.js → logos-DThdM9lk.js} +1 -1
- package/dist/assets/{page-layout-BMwpn87D.js → page-layout-D3Xo605Z.js} +1 -1
- package/dist/assets/plus-PHf8q-Ct.js +1 -0
- package/dist/assets/{popover-BIzq25oH.js → popover-BJRUGA_H.js} +1 -1
- package/dist/assets/provider-models-bz5y28rq.js +1 -0
- package/dist/assets/{react-ji6GGP_j.js → react-7ZHqQtEV.js} +1 -1
- package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
- package/dist/assets/{save-CMgYkJ-y.js → save-DJM5RRWW.js} +1 -1
- package/dist/assets/search-C91yH_6y.js +1 -0
- package/dist/assets/{security-config-Xi5DYW7j.js → security-config-T5zpg16O.js} +1 -1
- package/dist/assets/{select-Cz82gl01.js → select-DSkTc61S.js} +1 -1
- package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
- package/dist/assets/{status-dot-C7q1HvLH.js → status-dot-LNBlDu3q.js} +1 -1
- package/dist/assets/{switch-DYswvkYj.js → switch-Bo-Y46HZ.js} +1 -1
- package/dist/assets/tabs-custom-DXv507_2.js +1 -0
- package/dist/assets/{trash-2-DfXI7-ap.js → trash-2-DFZmW6Gg.js} +1 -1
- package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
- package/dist/assets/{useMutation-s2sn2yzh.js → useMutation-DrZrOgVL.js} +1 -1
- package/dist/assets/x-D7Q1yqSF.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +6 -6
- 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 +81 -6
- package/src/components/chat/adapters/chat-message.adapter.test.ts +393 -3
- package/src/components/chat/adapters/chat-message.partial-json.ts +89 -0
- package/src/components/chat/adapters/file-operation/card.ts +330 -0
- package/src/components/chat/adapters/file-operation/diff.ts +398 -0
- 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 +29 -32
- package/src/components/chat/containers/chat-message-list.container.tsx +1 -0
- 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 +176 -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/chat/useNcpAgentRuntime.test.tsx +90 -0
- 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 +25 -1
- package/src/lib/i18n.ts +21 -84
- package/src/lib/session-project/session-project.utils.ts +30 -0
- package/src/remote/remote-access-feedback.service.test.ts +18 -0
- package/src/remote/remote-access-feedback.service.ts +10 -1
- package/dist/assets/ChatPage-l2PYwCeB.js +0 -38
- package/dist/assets/DocBrowser-CIHLqoIm.js +0 -1
- package/dist/assets/MarketplacePage-TVeyVOuO.js +0 -1
- package/dist/assets/ModelConfig-Dg6F3Ldb.js +0 -1
- package/dist/assets/ProvidersList-f7bQdRxA.js +0 -1
- package/dist/assets/RemoteAccessPage-w_dY7P4T.js +0 -1
- package/dist/assets/RuntimeConfig-M4OKjmgU.js +0 -1
- package/dist/assets/chat-session-display-CGfXhJoT.js +0 -1
- package/dist/assets/i18n-CXBpwAwA.js +0 -1
- package/dist/assets/index-BahpXJg8.css +0 -1
- package/dist/assets/loader-circle-qgU4zQDw.js +0 -1
- package/dist/assets/plus-C9cYVbL-.js +0 -1
- package/dist/assets/provider-models-C8JQUd1E.js +0 -1
- package/dist/assets/search-sl1OeJFl.js +0 -1
- package/dist/assets/skeleton-rgIt7a5q.js +0 -1
- package/dist/assets/tabs-custom-DKYQxrx1.js +0 -1
- package/dist/assets/useConfirmDialog-CXDAxtRL.js +0 -1
- package/dist/assets/x-MIimOGs6.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,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
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type ChatSkillRecord,
|
|
16
16
|
type ChatThinkingLevel
|
|
17
17
|
} from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
18
|
+
import { deriveSelectedSkillsFromComposer } from '@/components/chat/chat-composer-state';
|
|
18
19
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
19
20
|
import {
|
|
20
21
|
CHAT_RECENT_MODELS_MIN_OPTIONS,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
} from '@/components/chat/chat-recent-skills.manager';
|
|
27
28
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
28
29
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
30
|
+
import type { SessionSkillEntryView } from '@/api/types';
|
|
29
31
|
import { t } from '@/lib/i18n';
|
|
30
32
|
import { toast } from 'sonner';
|
|
31
33
|
|
|
@@ -41,19 +43,17 @@ function buildThinkingLabels(): Record<ChatThinkingLevel, string> {
|
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function toSkillRecords(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
descriptionZh?: string;
|
|
49
|
-
origin?: string;
|
|
50
|
-
}>, officialBadgeLabel: string): ChatSkillRecord[] {
|
|
46
|
+
function toSkillRecords(
|
|
47
|
+
snapshotRecords: SessionSkillEntryView[],
|
|
48
|
+
scopeLabels: Record<SessionSkillEntryView['scope'], string>
|
|
49
|
+
): ChatSkillRecord[] {
|
|
51
50
|
return snapshotRecords.map((record) => ({
|
|
52
|
-
key: record.
|
|
53
|
-
label: record.
|
|
51
|
+
key: record.ref,
|
|
52
|
+
label: record.name,
|
|
53
|
+
scopeLabel: scopeLabels[record.scope],
|
|
54
54
|
description: record.description,
|
|
55
55
|
descriptionZh: record.descriptionZh,
|
|
56
|
-
badgeLabel: record.
|
|
56
|
+
badgeLabel: scopeLabels[record.scope]
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -87,20 +87,18 @@ export function ChatInputBarContainer() {
|
|
|
87
87
|
const inputBarRef = useRef<ChatInputBarHandle | null>(null);
|
|
88
88
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
89
89
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const skillScopeLabels = useMemo<Record<'project' | 'workspace', string>>(() => {
|
|
91
|
+
return {
|
|
92
|
+
project: t('chatSkillScopeProject'),
|
|
93
|
+
workspace: t('chatSkillScopeWorkspace'),
|
|
94
|
+
};
|
|
95
95
|
}, [language]);
|
|
96
96
|
const slashTexts = useMemo(
|
|
97
97
|
() => {
|
|
98
|
-
// Keep memo reactive to locale switches even though `t` is imported as a stable function.
|
|
99
|
-
const locale = language;
|
|
100
|
-
void locale;
|
|
101
98
|
return {
|
|
102
99
|
slashSkillSubtitle: t('chatSlashTypeSkill'),
|
|
103
100
|
slashSkillSpecLabel: t('chatSlashSkillSpec'),
|
|
101
|
+
slashSkillScopeLabel: t('chatSlashSkillScope'),
|
|
104
102
|
noSkillDescription: t('chatSkillsPickerNoDescription')
|
|
105
103
|
};
|
|
106
104
|
},
|
|
@@ -108,8 +106,8 @@ export function ChatInputBarContainer() {
|
|
|
108
106
|
);
|
|
109
107
|
|
|
110
108
|
const skillRecords = useMemo(
|
|
111
|
-
() => toSkillRecords(snapshot.skillRecords,
|
|
112
|
-
[snapshot.skillRecords,
|
|
109
|
+
() => toSkillRecords(snapshot.skillRecords, skillScopeLabels),
|
|
110
|
+
[snapshot.skillRecords, skillScopeLabels]
|
|
113
111
|
);
|
|
114
112
|
const modelRecords = useMemo(() => toModelRecords(snapshot.modelOptions), [snapshot.modelOptions]);
|
|
115
113
|
const recentModelValues = chatRecentModelsManager.resolveVisible({
|
|
@@ -136,10 +134,10 @@ export function ChatInputBarContainer() {
|
|
|
136
134
|
: hasModelOptions
|
|
137
135
|
? t('chatInputPlaceholder')
|
|
138
136
|
: t('chatModelNoOptions');
|
|
139
|
-
const recentModelsLabel =
|
|
140
|
-
const allModelsLabel =
|
|
141
|
-
const recentSkillsLabel =
|
|
142
|
-
const allSkillsLabel =
|
|
137
|
+
const recentModelsLabel = t('chatPickerRecentModels');
|
|
138
|
+
const allModelsLabel = t('chatPickerAllModels');
|
|
139
|
+
const recentSkillsLabel = t('chatPickerRecent');
|
|
140
|
+
const allSkillsLabel = t('chatPickerAllSkills');
|
|
143
141
|
|
|
144
142
|
const slashItems = useMemo(
|
|
145
143
|
() => buildChatSlashItems(skillRecords, slashQuery ?? '', slashTexts, recentSkillValues),
|
|
@@ -236,6 +234,12 @@ export function ChatInputBarContainer() {
|
|
|
236
234
|
}
|
|
237
235
|
});
|
|
238
236
|
|
|
237
|
+
const composerSelectedSkillCount = deriveSelectedSkillsFromComposer(snapshot.composerNodes).length;
|
|
238
|
+
const hasSendableDraft =
|
|
239
|
+
snapshot.draft.trim().length > 0 ||
|
|
240
|
+
snapshot.attachments.length > 0 ||
|
|
241
|
+
composerSelectedSkillCount > 0;
|
|
242
|
+
|
|
239
243
|
return (
|
|
240
244
|
<>
|
|
241
245
|
<ChatInputBar
|
|
@@ -296,14 +300,7 @@ export function ChatInputBarContainer() {
|
|
|
296
300
|
sendError: snapshot.sendError,
|
|
297
301
|
isSending: snapshot.isSending,
|
|
298
302
|
canStopGeneration: snapshot.canStopGeneration,
|
|
299
|
-
sendDisabled:
|
|
300
|
-
(
|
|
301
|
-
snapshot.draft.trim().length === 0 &&
|
|
302
|
-
snapshot.attachments.length === 0 &&
|
|
303
|
-
snapshot.selectedSkills.length === 0
|
|
304
|
-
) ||
|
|
305
|
-
!hasModelOptions ||
|
|
306
|
-
snapshot.sessionTypeUnavailable,
|
|
303
|
+
sendDisabled: !hasSendableDraft || !hasModelOptions || snapshot.sessionTypeUnavailable,
|
|
307
304
|
stopDisabled: !snapshot.canStopGeneration,
|
|
308
305
|
stopHint: resolvedStopHint,
|
|
309
306
|
sendButtonLabel: t('chatSend'),
|
|
@@ -40,6 +40,7 @@ function buildChatMessageAdapterTexts(
|
|
|
40
40
|
reasoningLabel: t("chatReasoning"),
|
|
41
41
|
toolCallLabel: t("chatToolCall"),
|
|
42
42
|
toolResultLabel: t("chatToolResult"),
|
|
43
|
+
toolInputLabel: t("chatToolInput"),
|
|
43
44
|
toolNoOutputLabel: t("chatToolNoOutput"),
|
|
44
45
|
toolOutputLabel: t("chatToolOutput"),
|
|
45
46
|
toolStatusPreparingLabel: t("chatToolStatusPreparing"),
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { useChatSessionProject } from '@/components/chat/hooks/use-chat-session-project';
|
|
5
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
updateSession: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('sonner', () => ({
|
|
12
|
+
toast: {
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('@/components/chat/hooks/use-chat-session-update', () => ({
|
|
18
|
+
useChatSessionUpdate: () => mocks.updateSession,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('useChatSessionProject', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
useChatInputStore.setState((state) => ({
|
|
24
|
+
snapshot: {
|
|
25
|
+
...state.snapshot,
|
|
26
|
+
pendingProjectRoot: null,
|
|
27
|
+
pendingProjectRootSessionKey: null,
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('stores the draft project root locally when the session does not exist yet', async () => {
|
|
37
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
38
|
+
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await result.current({
|
|
41
|
+
sessionKey: 'draft-session-1',
|
|
42
|
+
projectRoot: '/tmp/project-alpha',
|
|
43
|
+
persistToServer: false,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(mocks.updateSession).not.toHaveBeenCalled();
|
|
48
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
49
|
+
pendingProjectRoot: '/tmp/project-alpha',
|
|
50
|
+
pendingProjectRootSessionKey: 'draft-session-1',
|
|
51
|
+
});
|
|
52
|
+
expect(toast.success).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('keeps an explicit draft override when clearing the project root locally', async () => {
|
|
56
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
57
|
+
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await result.current({
|
|
60
|
+
sessionKey: 'draft-session-1',
|
|
61
|
+
projectRoot: null,
|
|
62
|
+
persistToServer: false,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(mocks.updateSession).not.toHaveBeenCalled();
|
|
67
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
68
|
+
pendingProjectRoot: null,
|
|
69
|
+
pendingProjectRootSessionKey: 'draft-session-1',
|
|
70
|
+
});
|
|
71
|
+
expect(toast.success).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('persists to the server and mirrors the updated project override locally for an existing session', async () => {
|
|
75
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
await result.current({
|
|
79
|
+
sessionKey: 'session-1',
|
|
80
|
+
projectRoot: '/tmp/project-beta',
|
|
81
|
+
persistToServer: true,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(mocks.updateSession).toHaveBeenCalledWith({
|
|
86
|
+
sessionKey: 'session-1',
|
|
87
|
+
patch: { projectRoot: '/tmp/project-beta' },
|
|
88
|
+
successMessage: 'Project directory updated',
|
|
89
|
+
});
|
|
90
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
91
|
+
pendingProjectRoot: '/tmp/project-beta',
|
|
92
|
+
pendingProjectRootSessionKey: 'session-1',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('persists clearing to the server and keeps the cleared override until session state catches up', async () => {
|
|
97
|
+
const { result } = renderHook(() => useChatSessionProject());
|
|
98
|
+
|
|
99
|
+
await act(async () => {
|
|
100
|
+
await result.current({
|
|
101
|
+
sessionKey: 'session-1',
|
|
102
|
+
projectRoot: null,
|
|
103
|
+
persistToServer: true,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mocks.updateSession).toHaveBeenCalledWith({
|
|
108
|
+
sessionKey: 'session-1',
|
|
109
|
+
patch: { projectRoot: null },
|
|
110
|
+
successMessage: 'Project directory cleared',
|
|
111
|
+
});
|
|
112
|
+
expect(useChatInputStore.getState().snapshot).toMatchObject({
|
|
113
|
+
pendingProjectRoot: null,
|
|
114
|
+
pendingProjectRootSessionKey: 'session-1',
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { toast } from 'sonner';
|
|
2
|
+
import { t } from '@/lib/i18n';
|
|
3
|
+
import { useChatSessionUpdate } from '@/components/chat/hooks/use-chat-session-update';
|
|
4
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
5
|
+
|
|
6
|
+
type UpdateChatSessionProjectParams = {
|
|
7
|
+
sessionKey: string;
|
|
8
|
+
projectRoot: string | null;
|
|
9
|
+
persistToServer: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useChatSessionProject() {
|
|
13
|
+
const updateSession = useChatSessionUpdate();
|
|
14
|
+
|
|
15
|
+
return async (params: UpdateChatSessionProjectParams): Promise<void> => {
|
|
16
|
+
const successMessage = params.projectRoot
|
|
17
|
+
? t('chatSessionProjectUpdated')
|
|
18
|
+
: t('chatSessionProjectCleared');
|
|
19
|
+
|
|
20
|
+
if (!params.persistToServer) {
|
|
21
|
+
useChatInputStore.getState().setSnapshot({
|
|
22
|
+
pendingProjectRoot: params.projectRoot,
|
|
23
|
+
pendingProjectRootSessionKey: params.sessionKey
|
|
24
|
+
});
|
|
25
|
+
toast.success(successMessage);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await updateSession({
|
|
30
|
+
sessionKey: params.sessionKey,
|
|
31
|
+
patch: { projectRoot: params.projectRoot },
|
|
32
|
+
successMessage,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
useChatInputStore.getState().setSnapshot({
|
|
36
|
+
pendingProjectRoot: params.projectRoot,
|
|
37
|
+
pendingProjectRootSessionKey: params.sessionKey,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts}
RENAMED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
|
+
import type { SessionPatchUpdate } from '@/api/types';
|
|
3
4
|
import { upsertNcpSessionSummaryInQueryClient } from '@/api/ncp-session-query-cache';
|
|
4
5
|
import { updateNcpSession } from '@/api/ncp-session';
|
|
5
6
|
import { t } from '@/lib/i18n';
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
+
type UpdateChatSessionParams = {
|
|
8
9
|
sessionKey: string;
|
|
9
|
-
|
|
10
|
+
patch: SessionPatchUpdate;
|
|
11
|
+
successMessage?: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
|
-
export function
|
|
14
|
+
export function useChatSessionUpdate() {
|
|
13
15
|
const queryClient = useQueryClient();
|
|
14
16
|
|
|
15
|
-
return async (params:
|
|
17
|
+
return async (params: UpdateChatSessionParams): Promise<void> => {
|
|
16
18
|
try {
|
|
17
|
-
const updated = await updateNcpSession(params.sessionKey,
|
|
19
|
+
const updated = await updateNcpSession(params.sessionKey, params.patch);
|
|
18
20
|
upsertNcpSessionSummaryInQueryClient(queryClient, updated);
|
|
19
|
-
toast.success(t('configSavedApplied'));
|
|
21
|
+
toast.success(params.successMessage ?? t('configSavedApplied'));
|
|
20
22
|
} catch (error) {
|
|
21
|
-
toast.error(
|
|
23
|
+
toast.error(
|
|
24
|
+
t('configSaveFailed') + ': ' + (error instanceof Error ? error.message : String(error)),
|
|
25
|
+
);
|
|
22
26
|
throw error;
|
|
23
27
|
}
|
|
24
28
|
};
|
|
@@ -44,7 +44,11 @@ export class ChatSessionListManager {
|
|
|
44
44
|
? sessionType.trim()
|
|
45
45
|
: defaultSessionType;
|
|
46
46
|
this.streamActionsManager.resetStreamState();
|
|
47
|
-
useChatInputStore.getState().setSnapshot({
|
|
47
|
+
useChatInputStore.getState().setSnapshot({
|
|
48
|
+
pendingSessionType: nextSessionType,
|
|
49
|
+
pendingProjectRoot: null,
|
|
50
|
+
pendingProjectRootSessionKey: null
|
|
51
|
+
});
|
|
48
52
|
this.uiManager.goToChatRoot();
|
|
49
53
|
};
|
|
50
54
|
|