@muyichengshayu/promptx 0.1.45 → 0.1.47
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 +12 -0
- package/apps/runner/src/runManager.js +38 -8
- package/apps/server/src/agentSessionDiscovery.js +604 -0
- package/apps/server/src/agents/claudeCodeRunner.js +4 -0
- package/apps/server/src/agents/codexRunner.js +8 -1
- package/apps/server/src/agents/index.js +8 -0
- package/apps/server/src/agents/openCodeRunner.js +4 -0
- package/apps/server/src/codex.js +49 -21
- package/apps/server/src/codexRoutes.js +8 -0
- package/apps/server/src/index.js +2 -1
- package/apps/web/dist/assets/CodexSessionManagerDialog-DZk_ClXW.js +3 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-B28M4Rwo.js +3 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-CeGj0idf.css +1 -0
- package/apps/web/dist/assets/{WorkbenchSettingsDialog-Dp35iSLy.js → WorkbenchSettingsDialog-DrXiTtO_.js} +1 -1
- package/apps/web/dist/assets/WorkbenchView-BdPC47JX.js +53 -0
- package/apps/web/dist/assets/WorkbenchView-BdpDI-HR.css +1 -0
- package/apps/web/dist/assets/index-Bx48HpZF.js +2 -0
- package/apps/web/dist/assets/index-DaELubyR.css +1 -0
- package/apps/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/shared/src/index.js +16 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-Da3ExvJ6.js +0 -1
- package/apps/web/dist/assets/TaskDiffReviewDialog-DjUU465b.js +0 -2
- package/apps/web/dist/assets/TaskDiffReviewDialog-Np_9C-g7.css +0 -1
- package/apps/web/dist/assets/WorkbenchView-CICtaY88.js +0 -47
- package/apps/web/dist/assets/WorkbenchView-CVt59HvL.css +0 -1
- package/apps/web/dist/assets/index-CWazC5a3.css +0 -1
- package/apps/web/dist/assets/index-d30njJCo.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.47
|
|
4
|
+
|
|
5
|
+
- 工作台新增“选中插入到编辑区”能力:在模型回复、项目源码查看与代码变更里都可直接拖选内容并一键插入右侧编辑区;其中模型回复按普通文本插入,更适合二次处理,源码与 diff 继续按代码上下文块插入。
|
|
6
|
+
- 统一选区插入按钮的交互与层级:三处场景改为共用同一套按钮组件,按钮从全局浮层收回到各自容器内,并在滚动、缩放与移动端视口变化时跟随选区重新定位,避免遮挡弹层头部或滚动后飘离内容区。
|
|
7
|
+
- 回复卡片代码块补齐复制入口,并继续收敛右侧导入文件阅读体验:代码块头部支持一键复制,导入文件正文从 `14px` 调整到 `12px`,同时把“已插入代码上下文到右侧编辑区”统一收口为更中性的插入成功提示。
|
|
8
|
+
|
|
9
|
+
## 0.1.46
|
|
10
|
+
|
|
11
|
+
- 项目管理新增本地会话发现与候选选择:`Codex / Claude Code / OpenCode` 都支持读取本机已有 session,创建或编辑项目时可直接按当前工作目录筛选并选择已有会话,不再只能手填 session ID。
|
|
12
|
+
- 补齐三种引擎的会话发现链路与回写一致性:`Codex` 改为直接读取带 WAL 的本地 SQLite,避免最新线程漏读;`OpenCode` 优先读取官方本地数据库,并在仅于最终结果返回 `threadId` 时也能正确回写到项目绑定。
|
|
13
|
+
- 收敛项目管理弹窗交互细节:目录比较逻辑抽到共享模块统一处理,候选会话列表增加短时缓存,减少同目录下反复打开弹窗时的重复请求与闪烁。
|
|
14
|
+
|
|
3
15
|
## 0.1.45
|
|
4
16
|
|
|
5
17
|
- 新增工作台“通用设置”:可配置 `Enter 发送 / Shift+Enter 发送 / 仅按钮发送` 三种发送行为,设置保存在本地,刷新后仍可延续;同时修复回车发送时编辑器会先插入一行空白再发送的问题。
|
|
@@ -48,6 +48,34 @@ function normalizeSession(payload = {}) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function applyThreadIdentity(session = {}, threadId = '') {
|
|
52
|
+
const value = String(threadId || '').trim()
|
|
53
|
+
if (!value) {
|
|
54
|
+
return session
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const engine = String(session?.engine || '').trim() || 'codex'
|
|
58
|
+
if (engine === 'codex') {
|
|
59
|
+
return {
|
|
60
|
+
...session,
|
|
61
|
+
codexThreadId: value,
|
|
62
|
+
engineThreadId: value,
|
|
63
|
+
running: true,
|
|
64
|
+
started: true,
|
|
65
|
+
updatedAt: nowIso(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...session,
|
|
71
|
+
engineSessionId: value,
|
|
72
|
+
engineThreadId: value,
|
|
73
|
+
running: true,
|
|
74
|
+
started: true,
|
|
75
|
+
updatedAt: nowIso(),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
51
79
|
function createRunSnapshot(context = {}) {
|
|
52
80
|
const stopControl = getChildStopDiagnostics(context.child)
|
|
53
81
|
return {
|
|
@@ -418,6 +446,15 @@ export function createRunManager(options = {}) {
|
|
|
418
446
|
}
|
|
419
447
|
|
|
420
448
|
async function handleStreamCompletion(context, result = {}) {
|
|
449
|
+
const completedThreadId = String(result?.threadId || '').trim()
|
|
450
|
+
if (completedThreadId) {
|
|
451
|
+
const nextSession = applyThreadIdentity(context.session, completedThreadId)
|
|
452
|
+
if (nextSession !== context.session) {
|
|
453
|
+
context.session = nextSession
|
|
454
|
+
queueEvent(context, createSessionUpdatedEnvelopeEvent(context.session))
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
421
458
|
if (context.stopRequestedAt) {
|
|
422
459
|
await finalizeRun(context, 'stopped', {
|
|
423
460
|
responseMessage: String(result?.message || '').trim(),
|
|
@@ -476,14 +513,7 @@ export function createRunManager(options = {}) {
|
|
|
476
513
|
return
|
|
477
514
|
}
|
|
478
515
|
|
|
479
|
-
context.session =
|
|
480
|
-
...context.session,
|
|
481
|
-
codexThreadId: value,
|
|
482
|
-
engineThreadId: value,
|
|
483
|
-
running: true,
|
|
484
|
-
started: true,
|
|
485
|
-
updatedAt: nowIso(),
|
|
486
|
-
}
|
|
516
|
+
context.session = applyThreadIdentity(context.session, value)
|
|
487
517
|
queueEvent(context, createSessionUpdatedEnvelopeEvent(context.session))
|
|
488
518
|
},
|
|
489
519
|
})
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import Database from 'better-sqlite3'
|
|
5
|
+
import { AGENT_ENGINES, normalizeComparablePath } from '../../../packages/shared/src/index.js'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_LIMIT = 80
|
|
8
|
+
const MAX_SCAN_FILES = 800
|
|
9
|
+
const MAX_DAT_FILE_SIZE = 8 * 1024 * 1024
|
|
10
|
+
const MAX_PREVIEW_LENGTH = 80
|
|
11
|
+
|
|
12
|
+
function normalizeLimit(value, fallback = DEFAULT_LIMIT) {
|
|
13
|
+
const limit = Math.max(1, Number(value) || fallback)
|
|
14
|
+
return Math.min(200, limit)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeText(value = '') {
|
|
18
|
+
return String(value || '').trim()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function safeStat(filePath = '') {
|
|
22
|
+
try {
|
|
23
|
+
return fs.statSync(filePath)
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeReadFile(filePath = '', maxBytes = MAX_DAT_FILE_SIZE) {
|
|
30
|
+
const stat = safeStat(filePath)
|
|
31
|
+
if (!stat?.isFile() || stat.size > maxBytes) {
|
|
32
|
+
return ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
37
|
+
} catch {
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJson(value) {
|
|
43
|
+
const text = normalizeText(value)
|
|
44
|
+
if (!text) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text)
|
|
50
|
+
} catch {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseMaybeJson(value) {
|
|
56
|
+
if (!value) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value === 'object') {
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return parseJson(value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractOpenCodePromptText(value) {
|
|
68
|
+
const parsed = parseMaybeJson(value)
|
|
69
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
70
|
+
return normalizeText(value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(parsed.prompt)) {
|
|
74
|
+
const parts = parsed.prompt
|
|
75
|
+
.map((item) => {
|
|
76
|
+
if (!item || typeof item !== 'object') {
|
|
77
|
+
return ''
|
|
78
|
+
}
|
|
79
|
+
return normalizeText(item.content || item.text || '')
|
|
80
|
+
})
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
return parts.join(' ').trim()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return extractMessageText(parsed)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sanitizeOpenCodeSessionLabel(value = '', cwd = '', sessionId = '') {
|
|
89
|
+
const text = normalizeText(value).replace(/\s+/g, ' ').trim()
|
|
90
|
+
if (text.length >= 2) {
|
|
91
|
+
return text
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return path.basename(normalizeText(cwd)) || normalizeText(sessionId)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toIsoDate(value) {
|
|
98
|
+
if (value === null || value === undefined || value === '') {
|
|
99
|
+
return ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
103
|
+
return value.toISOString()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
107
|
+
const timestamp = value > 1e12 ? value : value * 1000
|
|
108
|
+
const date = new Date(timestamp)
|
|
109
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : ''
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const text = normalizeText(value)
|
|
113
|
+
if (!text) {
|
|
114
|
+
return ''
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (/^\d+$/.test(text)) {
|
|
118
|
+
return toIsoDate(Number(text))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const date = new Date(text)
|
|
122
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : ''
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getSortTime(value = '') {
|
|
126
|
+
const time = Date.parse(normalizeText(value))
|
|
127
|
+
return Number.isFinite(time) ? time : 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createSessionCandidate(input = {}) {
|
|
131
|
+
const id = normalizeText(input.id)
|
|
132
|
+
if (!id) {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const cwd = normalizeText(input.cwd)
|
|
137
|
+
const label = normalizeText(input.label) || path.basename(cwd) || id
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
engine: input.engine,
|
|
141
|
+
label: label.length > MAX_PREVIEW_LENGTH ? `${label.slice(0, MAX_PREVIEW_LENGTH - 1)}…` : label,
|
|
142
|
+
cwd,
|
|
143
|
+
updatedAt: toIsoDate(input.updatedAt),
|
|
144
|
+
updatedAtSource: normalizeText(input.updatedAtSource),
|
|
145
|
+
source: normalizeText(input.source),
|
|
146
|
+
summary: normalizeText(input.summary),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getUpdatedAtPriority(value = '') {
|
|
151
|
+
return value === 'explicit' ? 1 : 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sortAndLimitCandidates(items = [], options = {}) {
|
|
155
|
+
const limit = normalizeLimit(options.limit)
|
|
156
|
+
const targetCwd = normalizeComparablePath(options.cwd)
|
|
157
|
+
const deduped = new Map()
|
|
158
|
+
|
|
159
|
+
items.forEach((item) => {
|
|
160
|
+
const candidate = createSessionCandidate(item)
|
|
161
|
+
if (!candidate) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const key = `${candidate.engine}:${candidate.id}`
|
|
166
|
+
const current = deduped.get(key)
|
|
167
|
+
if (!current) {
|
|
168
|
+
deduped.set(key, candidate)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const nextScore = Number(Boolean(candidate.cwd)) + Number(Boolean(candidate.label && candidate.label !== candidate.id))
|
|
173
|
+
const currentScore = Number(Boolean(current.cwd)) + Number(Boolean(current.label && current.label !== current.id))
|
|
174
|
+
const nextTimePriority = getUpdatedAtPriority(candidate.updatedAtSource)
|
|
175
|
+
const currentTimePriority = getUpdatedAtPriority(current.updatedAtSource)
|
|
176
|
+
if (
|
|
177
|
+
nextTimePriority > currentTimePriority
|
|
178
|
+
|| (nextTimePriority === currentTimePriority && getSortTime(candidate.updatedAt) > getSortTime(current.updatedAt))
|
|
179
|
+
|| nextScore > currentScore
|
|
180
|
+
) {
|
|
181
|
+
deduped.set(key, {
|
|
182
|
+
...current,
|
|
183
|
+
...candidate,
|
|
184
|
+
cwd: candidate.cwd || current.cwd,
|
|
185
|
+
label: candidate.label || current.label,
|
|
186
|
+
summary: candidate.summary || current.summary,
|
|
187
|
+
updatedAt: candidate.updatedAt || current.updatedAt,
|
|
188
|
+
updatedAtSource: candidate.updatedAtSource || current.updatedAtSource,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return [...deduped.values()]
|
|
194
|
+
.map((item) => ({
|
|
195
|
+
...item,
|
|
196
|
+
matchedCwd: Boolean(targetCwd && normalizeComparablePath(item.cwd) === targetCwd),
|
|
197
|
+
}))
|
|
198
|
+
.sort((left, right) => (
|
|
199
|
+
Number(right.matchedCwd) - Number(left.matchedCwd)
|
|
200
|
+
|| getUpdatedAtPriority(right.updatedAtSource) - getUpdatedAtPriority(left.updatedAtSource)
|
|
201
|
+
|| getSortTime(right.updatedAt) - getSortTime(left.updatedAt)
|
|
202
|
+
|| String(left.label || left.id).localeCompare(String(right.label || right.id), 'zh-CN')
|
|
203
|
+
))
|
|
204
|
+
.slice(0, limit)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function collectFiles(rootDir = '', options = {}) {
|
|
208
|
+
const root = normalizeText(rootDir)
|
|
209
|
+
if (!root || !safeStat(root)?.isDirectory()) {
|
|
210
|
+
return []
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const maxDepth = Math.max(0, Number(options.maxDepth) || 0)
|
|
214
|
+
const maxFiles = Math.max(1, Number(options.maxFiles) || MAX_SCAN_FILES)
|
|
215
|
+
const match = typeof options.match === 'function' ? options.match : () => true
|
|
216
|
+
const files = []
|
|
217
|
+
|
|
218
|
+
function visit(dir, depth) {
|
|
219
|
+
if (files.length >= maxFiles) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let entries = []
|
|
224
|
+
try {
|
|
225
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
226
|
+
} catch {
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (files.length >= maxFiles || entry.name.startsWith('.')) {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const entryPath = path.join(dir, entry.name)
|
|
236
|
+
if (entry.isDirectory()) {
|
|
237
|
+
if (depth < maxDepth) {
|
|
238
|
+
visit(entryPath, depth + 1)
|
|
239
|
+
}
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (entry.isFile() && match(entryPath, entry.name)) {
|
|
244
|
+
files.push(entryPath)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
visit(root, 0)
|
|
250
|
+
return files
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractMessageText(value, depth = 0) {
|
|
254
|
+
if (!value || depth > 4) {
|
|
255
|
+
return ''
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (typeof value === 'string') {
|
|
259
|
+
return normalizeText(value)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
for (const item of value) {
|
|
264
|
+
const text = extractMessageText(item, depth + 1)
|
|
265
|
+
if (text) {
|
|
266
|
+
return text
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return ''
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (typeof value !== 'object') {
|
|
273
|
+
return ''
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const key of ['text', 'content', 'message', 'prompt', 'summary']) {
|
|
277
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const text = extractMessageText(value[key], depth + 1)
|
|
282
|
+
if (text) {
|
|
283
|
+
return text
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return ''
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readJsonlPreview(filePath = '') {
|
|
291
|
+
const content = safeReadFile(filePath, 256 * 1024)
|
|
292
|
+
if (!content) {
|
|
293
|
+
return ''
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n').slice(0, 30)
|
|
297
|
+
for (const line of lines) {
|
|
298
|
+
const event = parseJson(line)
|
|
299
|
+
if (!event) {
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const type = normalizeText(event.type).toLowerCase()
|
|
304
|
+
if (type && type !== 'user') {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const text = extractMessageText(event)
|
|
309
|
+
if (text) {
|
|
310
|
+
return text.replace(/\s+/g, ' ').slice(0, MAX_PREVIEW_LENGTH)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return ''
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function decodeClaudeProjectPath(projectKey = '') {
|
|
318
|
+
const key = normalizeText(projectKey)
|
|
319
|
+
if (!key) {
|
|
320
|
+
return ''
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (key.startsWith('-')) {
|
|
324
|
+
return key.replace(/-/g, '/')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const driveMatch = key.match(/^([A-Za-z])-+/)
|
|
328
|
+
if (driveMatch) {
|
|
329
|
+
return `${driveMatch[1]}:${key.slice(driveMatch[0].length - 1).replace(/-/g, '\\')}`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return ''
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function listKnownClaudeCodeSessions(options = {}) {
|
|
336
|
+
const claudeHome = normalizeText(options.claudeHome || process.env.CLAUDE_HOME)
|
|
337
|
+
|| path.join(os.homedir(), '.claude')
|
|
338
|
+
const transcriptDir = path.join(claudeHome, 'transcripts')
|
|
339
|
+
const projectsDir = path.join(claudeHome, 'projects')
|
|
340
|
+
const items = []
|
|
341
|
+
|
|
342
|
+
collectFiles(transcriptDir, {
|
|
343
|
+
maxDepth: 0,
|
|
344
|
+
maxFiles: MAX_SCAN_FILES,
|
|
345
|
+
match: (filePath) => filePath.endsWith('.jsonl'),
|
|
346
|
+
}).forEach((filePath) => {
|
|
347
|
+
const stat = safeStat(filePath)
|
|
348
|
+
const id = path.basename(filePath, '.jsonl')
|
|
349
|
+
items.push({
|
|
350
|
+
id,
|
|
351
|
+
engine: AGENT_ENGINES.CLAUDE_CODE,
|
|
352
|
+
label: readJsonlPreview(filePath) || id,
|
|
353
|
+
updatedAt: stat?.mtime,
|
|
354
|
+
source: 'claude_transcripts',
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
collectFiles(projectsDir, {
|
|
359
|
+
maxDepth: 3,
|
|
360
|
+
maxFiles: MAX_SCAN_FILES,
|
|
361
|
+
match: (filePath) => filePath.endsWith('.jsonl'),
|
|
362
|
+
}).forEach((filePath) => {
|
|
363
|
+
const stat = safeStat(filePath)
|
|
364
|
+
const relativeParts = path.relative(projectsDir, filePath).split(path.sep).filter(Boolean)
|
|
365
|
+
const projectKey = relativeParts[0] || ''
|
|
366
|
+
const cwd = decodeClaudeProjectPath(projectKey)
|
|
367
|
+
const id = path.basename(filePath, '.jsonl')
|
|
368
|
+
items.push({
|
|
369
|
+
id,
|
|
370
|
+
engine: AGENT_ENGINES.CLAUDE_CODE,
|
|
371
|
+
label: readJsonlPreview(filePath) || path.basename(cwd) || id,
|
|
372
|
+
cwd,
|
|
373
|
+
updatedAt: stat?.mtime,
|
|
374
|
+
source: 'claude_projects',
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
return sortAndLimitCandidates(items, options)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getOpenCodeDataDirs(options = {}) {
|
|
382
|
+
if (options.openCodeDataDir) {
|
|
383
|
+
return [options.openCodeDataDir]
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (process.env.OPENCODE_DATA_DIR) {
|
|
387
|
+
return [process.env.OPENCODE_DATA_DIR]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const dirs = []
|
|
391
|
+
const home = os.homedir()
|
|
392
|
+
if (process.platform === 'darwin') {
|
|
393
|
+
dirs.push(path.join(home, 'Library', 'Application Support', 'ai.opencode.desktop'))
|
|
394
|
+
} else if (process.platform === 'win32') {
|
|
395
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming')
|
|
396
|
+
dirs.push(path.join(appData, 'ai.opencode.desktop'))
|
|
397
|
+
} else {
|
|
398
|
+
dirs.push(path.join(home, '.config', 'ai.opencode.desktop'))
|
|
399
|
+
}
|
|
400
|
+
dirs.push(path.join(home, '.opencode'))
|
|
401
|
+
return dirs
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getOpenCodeDbPaths(options = {}) {
|
|
405
|
+
const home = os.homedir()
|
|
406
|
+
|
|
407
|
+
if (options.openCodeDbPath) {
|
|
408
|
+
return [normalizeText(options.openCodeDbPath)].filter(Boolean)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const paths = []
|
|
412
|
+
if (process.env.OPENCODE_DB_PATH) {
|
|
413
|
+
paths.push(process.env.OPENCODE_DB_PATH)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (process.platform === 'win32') {
|
|
417
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local')
|
|
418
|
+
paths.push(path.join(localAppData, 'opencode', 'opencode.db'))
|
|
419
|
+
} else {
|
|
420
|
+
paths.push(path.join(home, '.local', 'share', 'opencode', 'opencode.db'))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
paths.push(path.join(home, '.opencode', 'opencode.db'))
|
|
424
|
+
|
|
425
|
+
return [...new Set(paths.map((item) => normalizeText(item)).filter(Boolean))]
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function decodeOpenCodeWorkspacePath(fileName = '') {
|
|
429
|
+
const name = normalizeText(fileName)
|
|
430
|
+
const match = name.match(/^opencode\.workspace\.([^.]+)(?:\..*)?\.dat$/)
|
|
431
|
+
if (!match) {
|
|
432
|
+
return ''
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const token = match[1]
|
|
436
|
+
if (token.startsWith('/') || /^[a-z]:[\\/]/i.test(token)) {
|
|
437
|
+
return token
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const decoded = Buffer.from(token, 'base64').toString('utf8')
|
|
442
|
+
if (
|
|
443
|
+
(decoded.startsWith('/') && decoded.split('/').filter(Boolean).length >= 3)
|
|
444
|
+
|| /^[a-z]:[\\/]/i.test(decoded)
|
|
445
|
+
) {
|
|
446
|
+
return decoded
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
return ''
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return ''
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function readOpenCodeDat(filePath = '') {
|
|
456
|
+
return parseJson(safeReadFile(filePath)) || {}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function loadOpenCodeSessionsFromDb(options = {}) {
|
|
460
|
+
const dbPath = getOpenCodeDbPaths(options).find((candidate) => safeStat(candidate)?.isFile())
|
|
461
|
+
if (!dbPath) {
|
|
462
|
+
return []
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let db
|
|
466
|
+
try {
|
|
467
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true })
|
|
468
|
+
const rows = db.prepare(`
|
|
469
|
+
select
|
|
470
|
+
id,
|
|
471
|
+
title,
|
|
472
|
+
directory,
|
|
473
|
+
time_updated,
|
|
474
|
+
time_created,
|
|
475
|
+
time_archived
|
|
476
|
+
from session
|
|
477
|
+
where time_archived is null
|
|
478
|
+
order by time_updated desc
|
|
479
|
+
limit ?
|
|
480
|
+
`).all(Math.max(50, normalizeLimit(options.limit) * 4))
|
|
481
|
+
|
|
482
|
+
return rows.map((row) => ({
|
|
483
|
+
id: normalizeText(row.id),
|
|
484
|
+
engine: AGENT_ENGINES.OPENCODE,
|
|
485
|
+
label: sanitizeOpenCodeSessionLabel(row.title, row.directory, row.id),
|
|
486
|
+
cwd: normalizeText(row.directory),
|
|
487
|
+
updatedAt: row.time_updated || row.time_created,
|
|
488
|
+
source: 'opencode_db',
|
|
489
|
+
summary: '',
|
|
490
|
+
}))
|
|
491
|
+
} catch {
|
|
492
|
+
return []
|
|
493
|
+
} finally {
|
|
494
|
+
try {
|
|
495
|
+
db?.close()
|
|
496
|
+
} catch {
|
|
497
|
+
// ignore close errors
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function addOpenCodeLayoutSessions(dat = {}, sourceFile = '', items = [], idToCwd = new Map()) {
|
|
503
|
+
const layout = parseMaybeJson(dat['layout.page'])
|
|
504
|
+
if (!layout || typeof layout !== 'object') {
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const lastProjectSession = layout.lastProjectSession && typeof layout.lastProjectSession === 'object'
|
|
509
|
+
? layout.lastProjectSession
|
|
510
|
+
: {}
|
|
511
|
+
Object.entries(lastProjectSession).forEach(([projectPath, value]) => {
|
|
512
|
+
const item = value && typeof value === 'object' ? value : {}
|
|
513
|
+
const id = normalizeText(item.id)
|
|
514
|
+
const cwd = normalizeText(item.directory || projectPath)
|
|
515
|
+
if (!id) {
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
idToCwd.set(id, cwd)
|
|
519
|
+
items.push({
|
|
520
|
+
id,
|
|
521
|
+
engine: AGENT_ENGINES.OPENCODE,
|
|
522
|
+
label: path.basename(cwd) || id,
|
|
523
|
+
cwd,
|
|
524
|
+
updatedAt: item.at,
|
|
525
|
+
updatedAtSource: item.at ? 'explicit' : 'inferred',
|
|
526
|
+
source: 'opencode_desktop',
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
const lastSession = layout.lastSession && typeof layout.lastSession === 'object'
|
|
531
|
+
? layout.lastSession
|
|
532
|
+
: {}
|
|
533
|
+
Object.entries(lastSession).forEach(([projectPath, idValue]) => {
|
|
534
|
+
const id = normalizeText(idValue)
|
|
535
|
+
const cwd = normalizeText(projectPath)
|
|
536
|
+
if (!id) {
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
idToCwd.set(id, cwd)
|
|
540
|
+
items.push({
|
|
541
|
+
id,
|
|
542
|
+
engine: AGENT_ENGINES.OPENCODE,
|
|
543
|
+
label: path.basename(cwd) || id,
|
|
544
|
+
cwd,
|
|
545
|
+
updatedAt: safeStat(sourceFile)?.mtime,
|
|
546
|
+
updatedAtSource: 'inferred',
|
|
547
|
+
source: 'opencode_desktop',
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function listKnownOpenCodeSessions(options = {}) {
|
|
553
|
+
const sqliteItems = loadOpenCodeSessionsFromDb(options)
|
|
554
|
+
if (sqliteItems.length) {
|
|
555
|
+
return sortAndLimitCandidates(sqliteItems, options)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const items = []
|
|
559
|
+
const idToCwd = new Map()
|
|
560
|
+
|
|
561
|
+
getOpenCodeDataDirs(options).forEach((dataDir) => {
|
|
562
|
+
collectFiles(dataDir, {
|
|
563
|
+
maxDepth: 0,
|
|
564
|
+
maxFiles: MAX_SCAN_FILES,
|
|
565
|
+
match: (filePath) => filePath.endsWith('.dat'),
|
|
566
|
+
}).forEach((filePath) => {
|
|
567
|
+
const stat = safeStat(filePath)
|
|
568
|
+
const dat = readOpenCodeDat(filePath)
|
|
569
|
+
const fileCwd = decodeOpenCodeWorkspacePath(path.basename(filePath))
|
|
570
|
+
|
|
571
|
+
addOpenCodeLayoutSessions(dat, filePath, items, idToCwd)
|
|
572
|
+
|
|
573
|
+
const fileSessionIds = [...new Set(
|
|
574
|
+
Object.keys(dat)
|
|
575
|
+
.map((key) => String(key || '').match(/^session:([^:]+):/)?.[1] || '')
|
|
576
|
+
.filter(Boolean)
|
|
577
|
+
)]
|
|
578
|
+
const mappedFileCwds = [...new Set(fileSessionIds.map((id) => idToCwd.get(id)).filter(Boolean))]
|
|
579
|
+
const workspaceCwd = mappedFileCwds.length === 1 ? mappedFileCwds[0] : fileCwd
|
|
580
|
+
|
|
581
|
+
Object.entries(dat).forEach(([key, value]) => {
|
|
582
|
+
const match = String(key || '').match(/^session:([^:]+):([^:]+)$/)
|
|
583
|
+
if (!match) {
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const [, id, field] = match
|
|
588
|
+
const cwd = idToCwd.get(id) || workspaceCwd
|
|
589
|
+
const text = field === 'prompt' ? extractOpenCodePromptText(value) : ''
|
|
590
|
+
items.push({
|
|
591
|
+
id,
|
|
592
|
+
engine: AGENT_ENGINES.OPENCODE,
|
|
593
|
+
label: sanitizeOpenCodeSessionLabel(text, cwd, id),
|
|
594
|
+
cwd,
|
|
595
|
+
updatedAt: stat?.mtime,
|
|
596
|
+
updatedAtSource: 'inferred',
|
|
597
|
+
source: 'opencode_desktop',
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
return sortAndLimitCandidates(items, options)
|
|
604
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createTurnCompletedEvent,
|
|
17
17
|
getAgentEngineLabel,
|
|
18
18
|
} from '../../../../packages/shared/src/index.js'
|
|
19
|
+
import { listKnownClaudeCodeSessions } from '../agentSessionDiscovery.js'
|
|
19
20
|
import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
|
|
20
21
|
|
|
21
22
|
const CLAUDE_CODE_BIN = process.env.CLAUDE_CODE_BIN || 'claude'
|
|
@@ -842,6 +843,9 @@ export const claudeCodeRunner = {
|
|
|
842
843
|
listKnownWorkspaces() {
|
|
843
844
|
return []
|
|
844
845
|
},
|
|
846
|
+
listKnownSessions(options = {}) {
|
|
847
|
+
return listKnownClaudeCodeSessions(options)
|
|
848
|
+
},
|
|
845
849
|
streamSessionPrompt(session, prompt, callbacks = {}) {
|
|
846
850
|
return streamPromptToClaudeCodeSession(session, prompt, callbacks)
|
|
847
851
|
},
|