@muyichengshayu/promptx 0.2.17 → 0.2.19
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 +10 -0
- package/apps/server/src/codexRoutes.js +12 -0
- package/apps/server/src/index.js +2 -0
- package/apps/server/src/workspaceFiles.js +170 -0
- package/apps/server/src/workspaceFiles.test.js +48 -0
- package/apps/web/dist/assets/{CodexSessionManagerDialog-D1PwOD4T.js → CodexSessionManagerDialog-C5Cht229.js} +3 -3
- package/apps/web/dist/assets/{TaskDiffReviewDialog-GKKv-IkZ.js → TaskDiffReviewDialog-D2l8KtRI.js} +5 -5
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-IWlkg3kU.js → WorkbenchSettingsDialog-DGj-_tuL.js} +1 -1
- package/apps/web/dist/assets/WorkbenchView-CebqJlAz.css +1 -0
- package/apps/web/dist/assets/WorkbenchView-ih1G4f79.js +58 -0
- package/apps/web/dist/assets/index-4LeM_JQe.js +2 -0
- package/apps/web/dist/assets/index-D9Ojj7Lp.css +1 -0
- package/apps/web/dist/assets/{vendor-markdown-9aQhqbjm.js → vendor-markdown-C2aIjAg9.js} +1 -1
- package/apps/web/dist/assets/{vendor-misc-u-M8sNMf.js → vendor-misc-DqxAnmp9.js} +30 -29
- package/apps/web/dist/assets/{vendor-router-Dn8q3tJM.js → vendor-router-4TN1NQvz.js} +1 -1
- package/apps/web/dist/assets/{vendor-shiki-core-1BqeTDsZ.js → vendor-shiki-core-UNANE_3B.js} +1 -1
- package/apps/web/dist/assets/vendor-shiki-lang-bash-OmlQ8O-f.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-c-Bf17QIO6.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-cpp-Dz7DCXQD.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-csharp-weekbih6.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-css-Cc89nYhT.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-diff-BKW5_WJa.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-dockerfile-DKFA3n9j.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-dotenv-BftIlyZY.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-fish-DTee4iUj.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-go-iEA7Y2np.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-html-fEXQcPZb.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-ini-BJNPDfrG.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-java-CZdBcwkx.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-javascript-5n6OdqL_.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-json-DkR7bRnH.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-less-DN-gfwMv.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-markdown-CgfEScgY.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-powershell-N9rELqmw.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-python-6ZggAICS.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-rust-BvkG5Wz9.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-sass-DFAXHETo.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-scss-ClLYCF3s.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-sql-CBrOCv6B.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-toml-C0vpllp6.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-tsx-DVg_XTuK.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-typescript-DV5oo9cD.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-vue-QYrDJbPe.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-xml-D4OZNqPk.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-lang-yaml-BZu-v6Um.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-theme-github-dark-default-DVxJvksW.js +1 -0
- package/apps/web/dist/assets/vendor-shiki-theme-github-light-BRz4x11q.js +1 -0
- package/apps/web/dist/assets/{vendor-tiptap-rwYdQb1L.js → vendor-tiptap-DLVi2vZE.js} +1 -1
- package/apps/web/dist/assets/{vendor-ui-BglsaDbv.js → vendor-ui-CnLXY_Zv.js} +31 -26
- package/apps/web/dist/index.html +4 -4
- package/package.json +1 -1
- package/apps/web/dist/assets/WorkbenchView-CK1snPBz.css +0 -1
- package/apps/web/dist/assets/WorkbenchView-dXHPTH_M.js +0 -58
- package/apps/web/dist/assets/index-BAfqUG7o.css +0 -1
- package/apps/web/dist/assets/index-DaIoquOV.js +0 -2
- package/apps/web/dist/assets/vendor-shiki-lang-bash-zsQu0xn-.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-c-BDF8vgks.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-cpp-DyDAaXl6.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-csharp-BWfTXJXw.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-css-DbAm1fUO.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-diff-DmGC1G26.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-dockerfile-3x-8396r.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-dotenv--IVOlX2Q.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-fish-Bpv3hhJP.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-go-CIGC59jP.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-html-Bt_TLLZN.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-ini-BIllfys4.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-java-CXxu9crF.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-javascript-Cha7GNZm.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-json-CCZCjDUh.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-less-DAToytC5.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-markdown-C6lwMXFp.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-powershell-lrpuM0ez.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-python-Bi9rmtY7.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-rust-GTGDbY6A.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-sass-DFtHupN8.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-scss-BrfBNyyi.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-sql-H6--iFN-.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-toml-aTteTWYL.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-tsx-DoKZm6AV.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-typescript-B5awnqri.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-vue-CnrgQ7Z5.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-xml-Bv-TNPJt.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-lang-yaml-DpxUxQ6M.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-theme-github-dark-default-DBX58552.js +0 -1
- package/apps/web/dist/assets/vendor-shiki-theme-github-light-RLAvZv0q.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.19
|
|
4
|
+
|
|
5
|
+
- 工作台 Response 卡片新增图片导出预览:右上角“图片”按钮会按 640px 版式生成 2x 高清 PNG,并在弹窗中展示,方便 PC 右键或手机长按保存、复制和转发。
|
|
6
|
+
- 优化导出图片的移动端可读性:生成图片时会使用导出专用排版,表格和代码块不再横向滚动导致截图不全;宽表和长代码会自动换行,优先保证内容完整。
|
|
7
|
+
- 收敛移动端消息卡片工具栏:Prompt / Response 卡片的“查看”“插入”“图片”等按钮在小屏下只保留图标,桌面端继续显示文字,避免按钮文案被挤成竖排。
|
|
8
|
+
|
|
9
|
+
## 0.2.18
|
|
10
|
+
|
|
11
|
+
- 源码浏览新增行作者提示:查看代码时默认启用 Git blame,鼠标悬停到源码行会在本行下方靠右显示最近修改作者、提交摘要、短 hash 与时间;非 Git 仓库、未跟踪文件、大文件和二进制文件会平稳降级为不可用提示。
|
|
12
|
+
|
|
3
13
|
## 0.2.17
|
|
4
14
|
|
|
5
15
|
- 修复 Windows 上 Codex 任务实际完成后工作台仍显示运行中、任务卡片持续转圈的问题:runner 在收到 `turn.completed` 后会进入短暂收尾等待;如果 Codex 子进程没有及时退出,会按完成状态落库并回收子进程,避免成功任务长时间卡在 `running`。
|
|
@@ -74,6 +74,7 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
74
74
|
listTaskSlugsByCodexSessionId = () => [],
|
|
75
75
|
listWorkspaceSuggestions,
|
|
76
76
|
listWorkspaceTree,
|
|
77
|
+
readWorkspaceFileBlame,
|
|
77
78
|
readWorkspaceFileContent,
|
|
78
79
|
resetPromptxCodexSession = () => null,
|
|
79
80
|
runDispatchService,
|
|
@@ -173,6 +174,17 @@ function registerCodexRoutes(app, options = {}) {
|
|
|
173
174
|
})
|
|
174
175
|
})
|
|
175
176
|
|
|
177
|
+
app.get('/api/codex/sessions/:sessionId/files/blame', async (request, reply) => {
|
|
178
|
+
const session = getPromptxCodexSessionById(request.params.sessionId)
|
|
179
|
+
if (!session) {
|
|
180
|
+
return reply.code(404).send({ messageKey: 'errors.sessionNotFound', message: '没有找到对应的 PromptX 项目。' })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return readWorkspaceFileBlame(session.cwd, {
|
|
184
|
+
path: request.query?.path,
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
176
188
|
app.get('/api/codex/sessions/:sessionId/files/content-search', async (request, reply) => {
|
|
177
189
|
const session = getPromptxCodexSessionById(request.params.sessionId)
|
|
178
190
|
if (!session) {
|
package/apps/server/src/index.js
CHANGED
|
@@ -55,6 +55,7 @@ import { listKnownSessionsByEngine, listKnownWorkspacesByEngine } from './agents
|
|
|
55
55
|
import {
|
|
56
56
|
listDirectoryPickerTree,
|
|
57
57
|
readWorkspaceFileContent,
|
|
58
|
+
readWorkspaceFileBlame,
|
|
58
59
|
listWorkspaceTree,
|
|
59
60
|
searchDirectoryPickerEntries,
|
|
60
61
|
searchWorkspaceFileContent,
|
|
@@ -434,6 +435,7 @@ registerCodexRoutes(app, {
|
|
|
434
435
|
listTaskSlugsByCodexSessionId,
|
|
435
436
|
listWorkspaceSuggestions: workspaceSuggestionService.listWorkspaceSuggestions,
|
|
436
437
|
listWorkspaceTree,
|
|
438
|
+
readWorkspaceFileBlame,
|
|
437
439
|
readWorkspaceFileContent,
|
|
438
440
|
resetPromptxCodexSession,
|
|
439
441
|
runDispatchService,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
4
5
|
import { createApiError } from './apiErrors.js'
|
|
5
6
|
|
|
6
7
|
const WORKSPACE_HIDDEN_DIRECTORY_NAMES = new Set([
|
|
@@ -41,6 +42,8 @@ const DEFAULT_CONTENT_SEARCH_LIMIT = 80
|
|
|
41
42
|
const MAX_SEARCH_VISITS = 20000
|
|
42
43
|
const DIRECTORY_PICKER_LIMIT = 240
|
|
43
44
|
const DEFAULT_FILE_PREVIEW_LIMIT = 200 * 1024
|
|
45
|
+
const DEFAULT_FILE_BLAME_LINE_LIMIT = Math.max(200, Number(process.env.PROMPTX_FILE_BLAME_LINE_LIMIT) || 5000)
|
|
46
|
+
const DEFAULT_FILE_BLAME_TIMEOUT_MS = Math.max(500, Number(process.env.PROMPTX_FILE_BLAME_TIMEOUT_MS) || 6000)
|
|
44
47
|
const MAX_IMAGE_PREVIEW_BYTES = 2 * 1024 * 1024
|
|
45
48
|
const MAX_CONTENT_SEARCH_FILE_BYTES = 1024 * 1024
|
|
46
49
|
const MAX_CONTENT_MATCHES_PER_FILE = 20
|
|
@@ -155,6 +158,17 @@ function createHttpError(message, statusCode = 400) {
|
|
|
155
158
|
return createApiError('', message, statusCode)
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
function createWorkspaceUnavailablePayload(target, reason = '', message = '') {
|
|
162
|
+
return {
|
|
163
|
+
cwd: target.root,
|
|
164
|
+
path: target.relativePath,
|
|
165
|
+
supported: false,
|
|
166
|
+
reason: String(reason || 'unavailable').trim() || 'unavailable',
|
|
167
|
+
message: String(message || '').trim(),
|
|
168
|
+
items: [],
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
function toPosixPath(value = '') {
|
|
159
173
|
return String(value || '').replace(/\\/g, '/')
|
|
160
174
|
}
|
|
@@ -216,6 +230,25 @@ function resolveWorkspaceTarget(workspacePath, relativePath = '') {
|
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
|
|
233
|
+
function runGit(workspacePath = '', args = [], options = {}) {
|
|
234
|
+
const result = spawnSync('git', ['-C', workspacePath, ...args], {
|
|
235
|
+
encoding: 'utf8',
|
|
236
|
+
maxBuffer: 12 * 1024 * 1024,
|
|
237
|
+
timeout: DEFAULT_FILE_BLAME_TIMEOUT_MS,
|
|
238
|
+
windowsHide: true,
|
|
239
|
+
...options,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
status: typeof result.status === 'number' ? result.status : 1,
|
|
244
|
+
stdout: String(result.stdout || ''),
|
|
245
|
+
stderr: String(result.stderr || ''),
|
|
246
|
+
signal: String(result.signal || ''),
|
|
247
|
+
errorCode: String(result.error?.code || ''),
|
|
248
|
+
timedOut: String(result.error?.code || '') === 'ETIMEDOUT',
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
function getPathType(absolutePath = '') {
|
|
220
253
|
try {
|
|
221
254
|
const stats = fs.statSync(absolutePath)
|
|
@@ -232,6 +265,78 @@ function getPathType(absolutePath = '') {
|
|
|
232
265
|
return ''
|
|
233
266
|
}
|
|
234
267
|
|
|
268
|
+
function getFileLineCount(absolutePath = '') {
|
|
269
|
+
const content = fs.readFileSync(absolutePath, 'utf8').replace(/\r\n/g, '\n')
|
|
270
|
+
return content ? content.split('\n').length : 0
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseGitBlamePorcelain(output = '') {
|
|
274
|
+
const items = []
|
|
275
|
+
const commitMeta = new Map()
|
|
276
|
+
let current = null
|
|
277
|
+
|
|
278
|
+
String(output || '').replace(/\r\n/g, '\n').split('\n').forEach((line) => {
|
|
279
|
+
const headerMatch = line.match(/^([0-9a-f]{40})\s+\d+\s+(\d+)(?:\s+\d+)?$/i)
|
|
280
|
+
if (headerMatch) {
|
|
281
|
+
const commit = headerMatch[1]
|
|
282
|
+
const lineNumber = Number(headerMatch[2]) || 0
|
|
283
|
+
const cached = commitMeta.get(commit) || {}
|
|
284
|
+
current = {
|
|
285
|
+
line: lineNumber,
|
|
286
|
+
commit,
|
|
287
|
+
author: cached.author || '',
|
|
288
|
+
authorMail: cached.authorMail || '',
|
|
289
|
+
authorTime: cached.authorTime || '',
|
|
290
|
+
summary: cached.summary || '',
|
|
291
|
+
}
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!current) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (line.startsWith('author ')) {
|
|
300
|
+
current.author = line.slice('author '.length)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
if (line.startsWith('author-mail ')) {
|
|
304
|
+
current.authorMail = line.slice('author-mail '.length).replace(/^<|>$/g, '')
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
if (line.startsWith('author-time ')) {
|
|
308
|
+
const timestamp = Number(line.slice('author-time '.length)) || 0
|
|
309
|
+
current.authorTime = timestamp > 0 ? new Date(timestamp * 1000).toISOString() : ''
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (line.startsWith('summary ')) {
|
|
313
|
+
current.summary = line.slice('summary '.length)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (line.startsWith('\t')) {
|
|
318
|
+
if (current.line > 0) {
|
|
319
|
+
const meta = {
|
|
320
|
+
author: current.author,
|
|
321
|
+
authorMail: current.authorMail,
|
|
322
|
+
authorTime: current.authorTime,
|
|
323
|
+
summary: current.summary,
|
|
324
|
+
}
|
|
325
|
+
commitMeta.set(current.commit, meta)
|
|
326
|
+
items.push({
|
|
327
|
+
line: current.line,
|
|
328
|
+
commit: current.commit,
|
|
329
|
+
shortCommit: current.commit.slice(0, 8),
|
|
330
|
+
...meta,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
current = null
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
return items.sort((left, right) => left.line - right.line)
|
|
338
|
+
}
|
|
339
|
+
|
|
235
340
|
function shouldIgnoreDirectory(entry) {
|
|
236
341
|
return entry?.isDirectory?.() && WORKSPACE_HIDDEN_DIRECTORY_NAMES.has(entry.name)
|
|
237
342
|
}
|
|
@@ -1025,6 +1130,71 @@ export function readWorkspaceFileContent(workspacePath, options = {}) {
|
|
|
1025
1130
|
}
|
|
1026
1131
|
}
|
|
1027
1132
|
|
|
1133
|
+
export function readWorkspaceFileBlame(workspacePath, options = {}) {
|
|
1134
|
+
const target = resolveWorkspaceTarget(workspacePath, options.path)
|
|
1135
|
+
|
|
1136
|
+
let stats
|
|
1137
|
+
try {
|
|
1138
|
+
stats = fs.statSync(target.absolutePath)
|
|
1139
|
+
} catch {
|
|
1140
|
+
throw createApiError('errors.fileNotFound', '文件不存在。', 404)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (!stats.isFile()) {
|
|
1144
|
+
throw createApiError('errors.fileOnly', '只能读取文件内容。')
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (stats.size > DEFAULT_FILE_PREVIEW_LIMIT) {
|
|
1148
|
+
return createWorkspaceUnavailablePayload(target, 'too_large', '文件较大,暂不加载行作者信息。')
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const previewBuffer = readFileSlice(target.absolutePath, Math.min(stats.size, DEFAULT_FILE_PREVIEW_LIMIT))
|
|
1152
|
+
const extension = path.extname(path.basename(target.absolutePath)).toLowerCase()
|
|
1153
|
+
const isKnownTextFile = TEXT_FILE_EXTENSIONS.has(extension)
|
|
1154
|
+
if (!isKnownTextFile && isLikelyBinaryBuffer(previewBuffer)) {
|
|
1155
|
+
return createWorkspaceUnavailablePayload(target, 'binary', '当前文件为二进制内容,暂不加载行作者信息。')
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const fileLineCount = getFileLineCount(target.absolutePath)
|
|
1159
|
+
if (fileLineCount > DEFAULT_FILE_BLAME_LINE_LIMIT) {
|
|
1160
|
+
return createWorkspaceUnavailablePayload(target, 'too_many_lines', '文件行数较多,暂不加载行作者信息。')
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const gitCheck = runGit(target.root, ['rev-parse', '--is-inside-work-tree'])
|
|
1164
|
+
if (gitCheck.status !== 0 || String(gitCheck.stdout || '').trim() !== 'true') {
|
|
1165
|
+
return createWorkspaceUnavailablePayload(target, 'not_git', '当前目录不是 Git 仓库。')
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const blame = runGit(target.root, [
|
|
1169
|
+
'blame',
|
|
1170
|
+
'--line-porcelain',
|
|
1171
|
+
'--',
|
|
1172
|
+
target.relativePath,
|
|
1173
|
+
])
|
|
1174
|
+
|
|
1175
|
+
if (blame.timedOut) {
|
|
1176
|
+
return createWorkspaceUnavailablePayload(target, 'timeout', '行作者信息加载超时。')
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (blame.status !== 0) {
|
|
1180
|
+
const stderr = String(blame.stderr || '').trim()
|
|
1181
|
+
if (/no such path|no such file|not in HEAD|fatal: no such/i.test(stderr)) {
|
|
1182
|
+
return createWorkspaceUnavailablePayload(target, 'untracked', '当前文件尚未被 Git 跟踪。')
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return createWorkspaceUnavailablePayload(target, 'failed', stderr || '行作者信息加载失败。')
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return {
|
|
1189
|
+
cwd: target.root,
|
|
1190
|
+
path: target.relativePath,
|
|
1191
|
+
supported: true,
|
|
1192
|
+
reason: '',
|
|
1193
|
+
message: '',
|
|
1194
|
+
items: parseGitBlamePorcelain(blame.stdout),
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1028
1198
|
export function listDirectoryPickerTree(options = {}) {
|
|
1029
1199
|
const isRootRequest = !String(options.path || '').trim()
|
|
1030
1200
|
const targetPath = normalizeDirectoryPickerPath(options.path) || getDirectoryPickerHomePath()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
2
3
|
import fs from 'node:fs'
|
|
3
4
|
import os from 'node:os'
|
|
4
5
|
import path from 'node:path'
|
|
@@ -7,12 +8,20 @@ import test from 'node:test'
|
|
|
7
8
|
import {
|
|
8
9
|
listDirectoryPickerTree,
|
|
9
10
|
listWorkspaceTree,
|
|
11
|
+
readWorkspaceFileBlame,
|
|
10
12
|
readWorkspaceFileContent,
|
|
11
13
|
searchWorkspaceFileContent,
|
|
12
14
|
searchWorkspaceEntries,
|
|
13
15
|
searchDirectoryPickerEntries,
|
|
14
16
|
} from './workspaceFiles.js'
|
|
15
17
|
|
|
18
|
+
function git(cwd, args = []) {
|
|
19
|
+
return execFileSync('git', ['-C', cwd, ...args], {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
}).trim()
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
test('listDirectoryPickerTree returns filesystem roots when path is empty', () => {
|
|
17
26
|
const payload = listDirectoryPickerTree()
|
|
18
27
|
|
|
@@ -277,3 +286,42 @@ test('readWorkspaceFileContent marks binary files', () => {
|
|
|
277
286
|
assert.equal(payload.binary, true)
|
|
278
287
|
assert.equal(payload.content, '')
|
|
279
288
|
})
|
|
289
|
+
|
|
290
|
+
test('readWorkspaceFileBlame returns author metadata per source line', () => {
|
|
291
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-workspace-blame-'))
|
|
292
|
+
const sourcePath = path.join(tempDir, 'src', 'main.js')
|
|
293
|
+
|
|
294
|
+
fs.mkdirSync(path.dirname(sourcePath), { recursive: true })
|
|
295
|
+
fs.writeFileSync(sourcePath, 'const answer = 42\nconsole.log(answer)\n', 'utf8')
|
|
296
|
+
git(tempDir, ['init'])
|
|
297
|
+
git(tempDir, ['config', 'user.email', 'promptx@example.com'])
|
|
298
|
+
git(tempDir, ['config', 'user.name', 'PromptX'])
|
|
299
|
+
git(tempDir, ['add', 'src/main.js'])
|
|
300
|
+
git(tempDir, ['commit', '-m', 'initial source'])
|
|
301
|
+
|
|
302
|
+
const payload = readWorkspaceFileBlame(tempDir, {
|
|
303
|
+
path: 'src/main.js',
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
assert.equal(payload.supported, true)
|
|
307
|
+
assert.equal(payload.path, 'src/main.js')
|
|
308
|
+
assert.equal(payload.items.length, 2)
|
|
309
|
+
assert.equal(payload.items[0].line, 1)
|
|
310
|
+
assert.equal(payload.items[0].author, 'PromptX')
|
|
311
|
+
assert.equal(payload.items[0].authorMail, 'promptx@example.com')
|
|
312
|
+
assert.equal(payload.items[0].summary, 'initial source')
|
|
313
|
+
assert.equal(payload.items[0].shortCommit.length, 8)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('readWorkspaceFileBlame returns unavailable payload outside git repository', () => {
|
|
317
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-workspace-blame-nongit-'))
|
|
318
|
+
fs.writeFileSync(path.join(tempDir, 'note.txt'), 'hello\n', 'utf8')
|
|
319
|
+
|
|
320
|
+
const payload = readWorkspaceFileBlame(tempDir, {
|
|
321
|
+
path: 'note.txt',
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
assert.equal(payload.supported, false)
|
|
325
|
+
assert.equal(payload.reason, 'not_git')
|
|
326
|
+
assert.deepEqual(payload.items, [])
|
|
327
|
+
})
|