@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/assets/{ChannelsList-ByHWHkQS.js → ChannelsList-Zeys_w43.js} +6 -6
  3. package/dist/assets/ChatPage-DWOU_8P6.js +43 -0
  4. package/dist/assets/DocBrowser-B9OaZjmg.js +1 -0
  5. package/dist/assets/{DocBrowser-3y_NHZ71.js → DocBrowser-BmtBLFU0.js} +1 -1
  6. package/dist/assets/{DocBrowserContext-CVJuwCcw.js → DocBrowserContext-YIKkPb76.js} +1 -1
  7. package/dist/assets/{LogoBadge-D8fyilO-.js → LogoBadge-F7ZWdxLT.js} +1 -1
  8. package/dist/assets/MarketplacePage-BfaTTqN6.js +1 -0
  9. package/dist/assets/{MarketplacePage-CmhsZXr1.js → MarketplacePage-Cd4faegU.js} +2 -2
  10. package/dist/assets/{McpMarketplacePage-C7PkCYbp.js → McpMarketplacePage-C09Ngs7O.js} +2 -2
  11. package/dist/assets/ModelConfig-DJgdcgvQ.js +1 -0
  12. package/dist/assets/ProvidersList-w0rVFIBf.js +1 -0
  13. package/dist/assets/RemoteAccessPage-BJ_ckkOV.js +1 -0
  14. package/dist/assets/RuntimeConfig-Cmn2xPQO.js +1 -0
  15. package/dist/assets/{SearchConfig-Dm7r2yfp.js → SearchConfig-BT13qpR_.js} +1 -1
  16. package/dist/assets/{SecretsConfig-BBP_mbQh.js → SecretsConfig-CvqEVn0B.js} +2 -2
  17. package/dist/assets/{SessionsConfig-6wNJloZN.js → SessionsConfig-DHHcYznk.js} +2 -2
  18. package/dist/assets/{book-open-B26jGBjY.js → book-open-CXoF5nQC.js} +1 -1
  19. package/dist/assets/chat-session-display-VW6ZMvZP.js +1 -0
  20. package/dist/assets/{chunk-JZWAC4HX-B-4B29RN.js → chunk-JZWAC4HX-CvRWvTy5.js} +1 -1
  21. package/dist/assets/{config-BaC29Qf-.js → config-DJswxxE8.js} +1 -1
  22. package/dist/assets/{createLucideIcon-DiFAvXmK.js → createLucideIcon-CjGHOWb6.js} +1 -1
  23. package/dist/assets/{dist-pCfWPG1A.js → dist-Cl2QB-2y.js} +1 -1
  24. package/dist/assets/{dist-kW_O3kyZ.js → dist-nqTTbVdA.js} +1 -1
  25. package/dist/assets/{external-link-D5-p-Gmm.js → external-link-tIO7zING.js} +1 -1
  26. package/dist/assets/{hash-BlwrSV0q.js → hash-JWUyl1pT.js} +1 -1
  27. package/dist/assets/i18n-CDHMXlRZ.js +1 -0
  28. package/dist/assets/index-BlH4-cBw.css +1 -0
  29. package/dist/assets/{index-DvKS3L9j.js → index-C6d0xmtm.js} +3 -3
  30. package/dist/assets/{label-RyXfZqkP.js → label-BIpeNu4r.js} +1 -1
  31. package/dist/assets/loader-circle-Cs8XVFTw.js +1 -0
  32. package/dist/assets/{logos-Bpl8QTgI.js → logos-DThdM9lk.js} +1 -1
  33. package/dist/assets/{page-layout--S0YBU0W.js → page-layout-D3Xo605Z.js} +1 -1
  34. package/dist/assets/plus-PHf8q-Ct.js +1 -0
  35. package/dist/assets/{popover-BEjfbEwy.js → popover-BJRUGA_H.js} +1 -1
  36. package/dist/assets/provider-models-bz5y28rq.js +1 -0
  37. package/dist/assets/{react-BuSP2-8B.js → react-7ZHqQtEV.js} +1 -1
  38. package/dist/assets/refresh-ccw-CC6-_QuL.js +1 -0
  39. package/dist/assets/{save-DPPPpD_c.js → save-DJM5RRWW.js} +1 -1
  40. package/dist/assets/search-C91yH_6y.js +1 -0
  41. package/dist/assets/{security-config-6t78Ph-I.js → security-config-T5zpg16O.js} +1 -1
  42. package/dist/assets/{select-CT50pzod.js → select-DSkTc61S.js} +1 -1
  43. package/dist/assets/skeleton-Dzg-HOiN.js +1 -0
  44. package/dist/assets/{status-dot-BbBqRHfh.js → status-dot-LNBlDu3q.js} +1 -1
  45. package/dist/assets/{switch-D3l6AcCk.js → switch-Bo-Y46HZ.js} +1 -1
  46. package/dist/assets/tabs-custom-DXv507_2.js +1 -0
  47. package/dist/assets/{trash-2-B2_AGVE3.js → trash-2-DFZmW6Gg.js} +1 -1
  48. package/dist/assets/useConfirmDialog-Bs5Ll17m.js +1 -0
  49. package/dist/assets/{useMutation-BzCrO8j-.js → useMutation-DrZrOgVL.js} +1 -1
  50. package/dist/assets/x-D7Q1yqSF.js +1 -0
  51. package/dist/index.html +18 -18
  52. package/package.json +3 -3
  53. package/src/api/ncp-session.test.ts +37 -0
  54. package/src/api/ncp-session.ts +29 -1
  55. package/src/api/server-path.ts +23 -0
  56. package/src/api/types.ts +41 -0
  57. package/src/components/chat/ChatConversationPanel.test.tsx +43 -7
  58. package/src/components/chat/ChatConversationPanel.tsx +23 -17
  59. package/src/components/chat/ChatSidebar.test.tsx +2 -2
  60. package/src/components/chat/ChatSidebar.tsx +2 -2
  61. package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +1 -0
  62. package/src/components/chat/adapters/chat-input-bar.adapter.ts +7 -2
  63. package/src/components/chat/adapters/chat-message-part.adapter.ts +13 -9
  64. package/src/components/chat/adapters/chat-message.adapter.test.ts +76 -4
  65. package/src/components/chat/adapters/{chat-message.file-operation-card.ts → file-operation/card.ts} +74 -181
  66. package/src/components/chat/adapters/{chat-message.file-operation-diff.ts → file-operation/diff.ts} +178 -188
  67. package/src/components/chat/adapters/file-operation/line-builder.ts +249 -0
  68. package/src/components/chat/adapters/file-operation/record-readers.ts +233 -0
  69. package/src/components/chat/chat-composer-state.ts +3 -3
  70. package/src/components/chat/chat-session-display.test.ts +21 -0
  71. package/src/components/chat/chat-session-display.ts +6 -1
  72. package/src/components/chat/containers/chat-input-bar.container.tsx +21 -24
  73. package/src/components/chat/hooks/use-chat-session-label.ts +19 -0
  74. package/src/components/chat/hooks/use-chat-session-project.test.tsx +117 -0
  75. package/src/components/chat/hooks/use-chat-session-project.ts +40 -0
  76. package/src/components/chat/{chat-session-label.service.ts → hooks/use-chat-session-update.ts} +11 -7
  77. package/src/components/chat/managers/chat-session-list.manager.ts +5 -1
  78. package/src/components/chat/ncp/NcpChatPage.tsx +55 -17
  79. package/src/components/chat/ncp/ncp-chat-page-data.test.ts +33 -0
  80. package/src/components/chat/ncp/ncp-chat-page-data.ts +21 -15
  81. package/src/components/chat/ncp/ncp-session-adapter.test.ts +3 -0
  82. package/src/components/chat/ncp/ncp-session-adapter.ts +16 -0
  83. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +63 -0
  84. package/src/components/chat/session-header/chat-session-header-actions.tsx +95 -0
  85. package/src/components/chat/session-header/chat-session-header-menu-item.tsx +35 -0
  86. package/src/components/chat/session-header/chat-session-project-badge.test.tsx +66 -0
  87. package/src/components/chat/session-header/chat-session-project-badge.tsx +102 -0
  88. package/src/components/chat/session-header/chat-session-project-dialog.tsx +34 -0
  89. package/src/components/chat/stores/chat-input.store.ts +6 -3
  90. package/src/components/chat/stores/chat-thread.store.ts +6 -2
  91. package/src/components/path-picker/server-path-picker-dialog.test.tsx +92 -0
  92. package/src/components/path-picker/server-path-picker-dialog.tsx +282 -0
  93. package/src/hooks/server-path/use-server-path-browse.ts +19 -0
  94. package/src/hooks/useConfig.ts +26 -1
  95. package/src/lib/i18n/i18n-language-owner.ts +94 -0
  96. package/src/lib/i18n/i18n.path-picker.ts +12 -0
  97. package/src/lib/i18n.chat.ts +23 -0
  98. package/src/lib/i18n.ts +21 -84
  99. package/src/lib/session-project/session-project.utils.ts +30 -0
  100. package/dist/assets/ChatPage-FdT3pDnw.js +0 -42
  101. package/dist/assets/DocBrowser-CMdPdbZj.js +0 -1
  102. package/dist/assets/MarketplacePage-9oKmxN2n.js +0 -1
  103. package/dist/assets/ModelConfig-DmCY6jWM.js +0 -1
  104. package/dist/assets/ProvidersList-ClT-34aX.js +0 -1
  105. package/dist/assets/RemoteAccessPage-B6hUZl1O.js +0 -1
  106. package/dist/assets/RuntimeConfig-C5aqliGk.js +0 -1
  107. package/dist/assets/chat-session-display-Bjmn4aIZ.js +0 -1
  108. package/dist/assets/i18n-CSytxMFI.js +0 -1
  109. package/dist/assets/index-CUy6doWo.css +0 -1
  110. package/dist/assets/loader-circle-B2J777gj.js +0 -1
  111. package/dist/assets/plus-CM9XJ0Tf.js +0 -1
  112. package/dist/assets/provider-models-C8JQUd1E.js +0 -1
  113. package/dist/assets/search-Ctaw34Kp.js +0 -1
  114. package/dist/assets/skeleton-Bycyb0zU.js +0 -1
  115. package/dist/assets/tabs-custom-TZQ5WPWP.js +0 -1
  116. package/dist/assets/useConfirmDialog-BDpdjfIO.js +0 -1
  117. package/dist/assets/x-CHOBE-63.js +0 -1
  118. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-WtpHP_DW.js} +0 -0
  119. /package/dist/assets/{config-layout-B-7erZRN.js → config-layout-LQ10ozRC.js} +0 -0
  120. /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<{ spec: string; label?: string }>
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.spec, 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)?.label || 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 [session.key, sessionDisplayName(session)]
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(snapshotRecords: Array<{
46
- spec: string;
47
- label?: string;
48
- description?: string;
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.spec,
54
- label: record.label || record.spec,
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.origin === 'builtin' ? officialBadgeLabel : undefined
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 officialSkillBadgeLabel = useMemo(() => {
92
- // Keep memo reactive to locale switches even though `t` is imported as a stable function.
93
- const locale = language;
94
- void locale;
95
- return t('chatSkillsPickerOfficial');
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, officialSkillBadgeLabel),
113
- [snapshot.skillRecords, officialSkillBadgeLabel]
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 = language === 'zh' ? '最近选择' : 'Recent';
141
- const allModelsLabel = language === 'zh' ? '全部模型' : 'All models';
142
- const recentSkillsLabel = language === 'zh' ? '最近使用' : 'Recent';
143
- const allSkillsLabel = language === 'zh' ? '全部技能' : 'All skills';
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
+ }