@shawnstack/quickforge 1.4.1 → 1.5.0
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/README.md +12 -12
- package/bin/quickforge.mjs +9 -0
- package/dist/assets/AgentProfilesPage-DUmXUxjA.js +1 -0
- package/dist/assets/ChatPanelHost-Syx0SSLe.js +242 -0
- package/dist/assets/PluginsPage-kiBq0gOT.js +1 -0
- package/dist/assets/ScheduledTasksPage-Dw4-tgp9.js +2 -0
- package/dist/assets/SharedConversationPage-CaE9bNb9.js +1 -0
- package/dist/assets/TerminalDock-BYJcp8Ts.js +2 -0
- package/dist/assets/WorkspaceInspector-Bzmv8Cvi.js +3 -0
- package/dist/assets/WorkspaceReaderDialog-BJo_KEWi.js +1 -0
- package/dist/assets/diff-line-counts-BZoYp5ai.js +10 -0
- package/dist/assets/icons-47L5YLKz.js +1 -0
- package/dist/assets/index-CqfScETb.js +1200 -0
- package/dist/assets/index-DzkBgHZf.css +3 -0
- package/dist/assets/{monaco-evITXh-m.js → monaco-CGq6uVF1.js} +1 -1
- package/dist/assets/{react-vendor-Mthyt1p4.js → react-vendor-DunfCFfp.js} +1 -1
- package/dist/favicon.svg +16 -1
- package/dist/index.html +5 -5
- package/dist/manifest.webmanifest +30 -30
- package/package.json +3 -2
- package/server/acp/server.mjs +921 -0
- package/server/agent-manager.mjs +198 -32
- package/server/agent-profile-files.mjs +179 -0
- package/server/agent-profiles.mjs +59 -5
- package/server/auto-compaction.mjs +82 -39
- package/server/channels/process-channel.mjs +278 -0
- package/server/channels/providers/wechat.mjs +271 -0
- package/server/channels/registry.mjs +58 -0
- package/server/custom-commands.mjs +13 -1
- package/server/frontmatter.mjs +167 -0
- package/server/index.mjs +52 -3
- package/server/project-config.mjs +43 -6
- package/server/routes/agent-profiles.mjs +6 -2
- package/server/routes/agent.mjs +12 -1
- package/server/routes/channels.mjs +145 -0
- package/server/routes/models.mjs +68 -0
- package/server/routes/project.mjs +2 -2
- package/server/routes/scheduled-tasks.mjs +6 -5
- package/server/routes/storage.mjs +4 -2
- package/server/routes/system.mjs +27 -0
- package/server/routes/tools.mjs +17 -6
- package/server/routes/workspace.mjs +138 -0
- package/server/session-utils.mjs +10 -2
- package/server/storage.mjs +29 -2
- package/server/system-prompt.mjs +1 -0
- package/server/tools/definitions.mjs +18 -0
- package/server/tools/index.mjs +83 -0
- package/server/utils/package-update.mjs +156 -0
- package/dist/assets/AgentProfilesPage-CNK5PxA3.js +0 -1
- package/dist/assets/ChatPanelHost-FqPQwwMO.js +0 -217
- package/dist/assets/PluginsPage-BCu1Ept0.js +0 -1
- package/dist/assets/ScheduledTasksPage-Bx04rjui.js +0 -2
- package/dist/assets/SharedConversationPage-55vX9sqe.js +0 -1
- package/dist/assets/TerminalDock-DLN_pLkJ.js +0 -2
- package/dist/assets/WorkspaceInspector-DoemHHnY.js +0 -3
- package/dist/assets/WorkspaceReaderDialog-C6xUHBCw.js +0 -6
- package/dist/assets/icons-BWtivFsx.js +0 -1
- package/dist/assets/index-CxOHP41X.css +0 -3
- package/dist/assets/index-Dcf73EL8.js +0 -895
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { Readable, Writable } from 'node:stream'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { ndJsonStream, AgentSideConnection, PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
|
8
|
+
import {
|
|
9
|
+
createAgent,
|
|
10
|
+
runPrompt,
|
|
11
|
+
abortRun,
|
|
12
|
+
destroyAgent,
|
|
13
|
+
restoreAgent,
|
|
14
|
+
getSessionState,
|
|
15
|
+
getSessionEventBus,
|
|
16
|
+
listSessions as listAgentSessions,
|
|
17
|
+
approveToolCall,
|
|
18
|
+
rejectToolCall,
|
|
19
|
+
updateSessionModel,
|
|
20
|
+
updateSessionThinkingLevel,
|
|
21
|
+
} from '../agent-manager.mjs'
|
|
22
|
+
import { getActiveProject, getDefaultWorkspaceRoot, readProjectConfig, setActiveProjectPath, sameProjectPath } from '../project-config.mjs'
|
|
23
|
+
import { readSessionValue, readStore } from '../storage.mjs'
|
|
24
|
+
import { logger } from '../utils/logger.mjs'
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
27
|
+
const __dirname = path.dirname(__filename)
|
|
28
|
+
const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json')
|
|
29
|
+
|
|
30
|
+
const APP_NAME = 'QuickForge'
|
|
31
|
+
const DEFAULT_MODE_ID = 'default'
|
|
32
|
+
const MODEL_CONFIG_ID = 'quickforge.model'
|
|
33
|
+
const THINKING_LEVEL_CONFIG_ID = 'quickforge.thinkingLevel'
|
|
34
|
+
const THINKING_LEVELS = [
|
|
35
|
+
{ value: 'off', name: 'Off' },
|
|
36
|
+
{ value: 'low', name: 'Low' },
|
|
37
|
+
{ value: 'medium', name: 'Medium' },
|
|
38
|
+
{ value: 'high', name: 'High' },
|
|
39
|
+
{ value: 'xhigh', name: 'Extra High' },
|
|
40
|
+
]
|
|
41
|
+
const EVENT_TEXT_LIMIT = 64 * 1024
|
|
42
|
+
const DOCUMENT_CONTEXT_LIMIT = 24 * 1024
|
|
43
|
+
const DOCUMENT_PREVIEW_LIMIT = 4 * 1024
|
|
44
|
+
const MAX_CONTEXT_DOCUMENTS = 4
|
|
45
|
+
const DANGEROUS_WORKSPACE_ROOTS = new Set([
|
|
46
|
+
path.parse(process.cwd()).root,
|
|
47
|
+
os.homedir(),
|
|
48
|
+
].map((item) => path.resolve(item).toLowerCase()))
|
|
49
|
+
|
|
50
|
+
const pendingPrompts = new Map()
|
|
51
|
+
const pendingPermissions = new Set()
|
|
52
|
+
const acpSessions = new Map()
|
|
53
|
+
const acpDocuments = new Map()
|
|
54
|
+
let focusedDocumentUri = null
|
|
55
|
+
|
|
56
|
+
let packageInfoPromise = null
|
|
57
|
+
|
|
58
|
+
async function readPackageInfo() {
|
|
59
|
+
if (!packageInfoPromise) {
|
|
60
|
+
packageInfoPromise = fs.readFile(packageJsonPath, 'utf8')
|
|
61
|
+
.then((text) => JSON.parse(text))
|
|
62
|
+
.catch(() => ({ name: '@shawnstack/quickforge', version: '0.0.0' }))
|
|
63
|
+
}
|
|
64
|
+
return packageInfoPromise
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizePromptText(prompt = []) {
|
|
68
|
+
const parts = []
|
|
69
|
+
for (const block of Array.isArray(prompt) ? prompt : []) {
|
|
70
|
+
if (!block || typeof block !== 'object') continue
|
|
71
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
72
|
+
parts.push(block.text)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (block.type === 'resource_link') {
|
|
76
|
+
parts.push(`[resource: ${block.uri || block.name || 'unknown'}]`)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (block.type === 'resource' && block.resource?.text) {
|
|
80
|
+
parts.push(block.resource.text)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
parts.push(`[unsupported ${block.type || 'content'} content omitted]`)
|
|
84
|
+
}
|
|
85
|
+
return parts.join('\n\n').trim()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function textContent(text) {
|
|
89
|
+
return { type: 'text', text: String(text ?? '') }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function truncateText(value, limit = EVENT_TEXT_LIMIT) {
|
|
93
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value ?? '', null, 2)
|
|
94
|
+
if (text.length <= limit) return text
|
|
95
|
+
return `${text.slice(0, limit)}\n… truncated ${text.length - limit} characters`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function messageContentText(message) {
|
|
99
|
+
const content = message?.content
|
|
100
|
+
if (typeof content === 'string') return content
|
|
101
|
+
if (Array.isArray(content)) {
|
|
102
|
+
return content.map((part) => {
|
|
103
|
+
if (typeof part === 'string') return part
|
|
104
|
+
if (part?.type === 'text' && typeof part.text === 'string') return part.text
|
|
105
|
+
if (typeof part?.text === 'string') return part.text
|
|
106
|
+
return ''
|
|
107
|
+
}).filter(Boolean).join('\n')
|
|
108
|
+
}
|
|
109
|
+
return ''
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function eventMessageText(event) {
|
|
113
|
+
return messageContentText(event?.message)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function documentUriFromParams(params = {}) {
|
|
117
|
+
return params.textDocument?.uri || params.document?.uri || params.uri || params.textDocumentUri || null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function documentTextFromParams(params = {}) {
|
|
121
|
+
if (typeof params.text === 'string') return params.text
|
|
122
|
+
if (typeof params.content === 'string') return params.content
|
|
123
|
+
if (typeof params.textDocument?.text === 'string') return params.textDocument.text
|
|
124
|
+
if (typeof params.document?.text === 'string') return params.document.text
|
|
125
|
+
const fullChange = Array.isArray(params.contentChanges)
|
|
126
|
+
? params.contentChanges.find((change) => change && typeof change.text === 'string' && !change.range)
|
|
127
|
+
: null
|
|
128
|
+
return fullChange?.text ?? null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function documentLanguageFromParams(params = {}) {
|
|
132
|
+
return params.textDocument?.languageId || params.document?.languageId || params.languageId || ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function documentVersionFromParams(params = {}) {
|
|
136
|
+
return params.textDocument?.version ?? params.document?.version ?? params.version ?? null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function updateAcpDocument(params = {}, reason = 'open') {
|
|
140
|
+
const uri = documentUriFromParams(params)
|
|
141
|
+
if (!uri) return
|
|
142
|
+
const previous = acpDocuments.get(uri) || { uri, text: '', languageId: '', version: null, updatedAt: new Date().toISOString() }
|
|
143
|
+
const text = documentTextFromParams(params)
|
|
144
|
+
const next = {
|
|
145
|
+
...previous,
|
|
146
|
+
uri,
|
|
147
|
+
languageId: documentLanguageFromParams(params) || previous.languageId || '',
|
|
148
|
+
version: documentVersionFromParams(params) ?? previous.version ?? null,
|
|
149
|
+
text: typeof text === 'string' ? text : previous.text,
|
|
150
|
+
reason,
|
|
151
|
+
updatedAt: new Date().toISOString(),
|
|
152
|
+
}
|
|
153
|
+
acpDocuments.set(uri, next)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function closeAcpDocument(params = {}) {
|
|
157
|
+
const uri = documentUriFromParams(params)
|
|
158
|
+
if (!uri) return
|
|
159
|
+
acpDocuments.delete(uri)
|
|
160
|
+
if (focusedDocumentUri === uri) focusedDocumentUri = null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function focusAcpDocument(params = {}) {
|
|
164
|
+
const uri = documentUriFromParams(params)
|
|
165
|
+
if (!uri) return
|
|
166
|
+
focusedDocumentUri = uri
|
|
167
|
+
if (!acpDocuments.has(uri)) updateAcpDocument({ ...params, text: '' }, 'focus')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatDocumentContext(doc, limit) {
|
|
171
|
+
if (!doc) return ''
|
|
172
|
+
const header = [`URI: ${doc.uri}`]
|
|
173
|
+
if (doc.languageId) header.push(`Language: ${doc.languageId}`)
|
|
174
|
+
if (doc.version !== null && doc.version !== undefined) header.push(`Version: ${doc.version}`)
|
|
175
|
+
const text = truncateText(doc.text || '', limit)
|
|
176
|
+
return `${header.join('\n')}\nContent:\n${text}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function sessionContext(sessionId) {
|
|
180
|
+
return acpSessions.get(sessionId) || null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function acpContextPrompt(sessionId) {
|
|
184
|
+
const parts = []
|
|
185
|
+
const session = sessionContext(sessionId)
|
|
186
|
+
if (session?.cwd) parts.push(`Workspace root: ${session.cwd}`)
|
|
187
|
+
if (Array.isArray(session?.additionalDirectories) && session.additionalDirectories.length > 0) {
|
|
188
|
+
parts.push(`Additional workspace roots:\n${session.additionalDirectories.map((dir) => `- ${dir}`).join('\n')}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const docs = []
|
|
192
|
+
if (focusedDocumentUri && acpDocuments.has(focusedDocumentUri)) docs.push(acpDocuments.get(focusedDocumentUri))
|
|
193
|
+
for (const doc of acpDocuments.values()) {
|
|
194
|
+
if (docs.length >= MAX_CONTEXT_DOCUMENTS) break
|
|
195
|
+
if (!docs.some((item) => item.uri === doc.uri)) docs.push(doc)
|
|
196
|
+
}
|
|
197
|
+
if (docs.length > 0) {
|
|
198
|
+
const renderedDocs = docs.map((doc, index) => {
|
|
199
|
+
const limit = index === 0 ? DOCUMENT_CONTEXT_LIMIT : DOCUMENT_PREVIEW_LIMIT
|
|
200
|
+
return `<document${doc.uri === focusedDocumentUri ? ' focused="true"' : ''}>\n${formatDocumentContext(doc, limit)}\n</document>`
|
|
201
|
+
}).join('\n\n')
|
|
202
|
+
parts.push(`Open editor documents:\n${renderedDocs}`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (parts.length === 0) return ''
|
|
206
|
+
return `<acp_context>\n${parts.join('\n\n')}\n</acp_context>`
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function withAcpContext(sessionId, message) {
|
|
210
|
+
const context = acpContextPrompt(sessionId)
|
|
211
|
+
return context ? `${context}\n\n${message}` : message
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function historyMessageUpdate(message, index) {
|
|
215
|
+
const role = message?.role
|
|
216
|
+
const text = messageContentText(message)
|
|
217
|
+
if (!text) return null
|
|
218
|
+
if (role === 'assistant') {
|
|
219
|
+
return {
|
|
220
|
+
sessionUpdate: 'agent_message_chunk',
|
|
221
|
+
content: textContent(text),
|
|
222
|
+
messageId: `history-assistant-${index}`,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (role === 'user' || role === 'user-with-attachments') {
|
|
226
|
+
return {
|
|
227
|
+
sessionUpdate: 'user_message_chunk',
|
|
228
|
+
content: textContent(text),
|
|
229
|
+
messageId: `history-user-${index}`,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function convertMessagesToHistoryUpdates(messages = []) {
|
|
236
|
+
return (Array.isArray(messages) ? messages : [])
|
|
237
|
+
.map((message, index) => historyMessageUpdate(message, index))
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function replaySessionHistory(sessionId, conn) {
|
|
242
|
+
if (!conn?.sessionUpdate) return
|
|
243
|
+
const state = getSessionState(sessionId)
|
|
244
|
+
const messages = state?.messages || (await readSessionValue(sessionId))?.messages || []
|
|
245
|
+
for (const update of convertMessagesToHistoryUpdates(messages)) {
|
|
246
|
+
await conn.sessionUpdate({ sessionId, update })
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function toolKind(toolName = '') {
|
|
251
|
+
if (/read|list|cat|show/i.test(toolName)) return 'read'
|
|
252
|
+
if (/write|edit|patch|present/i.test(toolName)) return 'edit'
|
|
253
|
+
if (/grep|search|find/i.test(toolName)) return 'search'
|
|
254
|
+
if (/run|command|terminal|subagent/i.test(toolName)) return 'execute'
|
|
255
|
+
if (/fetch|http|web/i.test(toolName)) return 'fetch'
|
|
256
|
+
return 'other'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function toolTitle(event) {
|
|
260
|
+
const name = event?.toolName || event?.name || 'tool'
|
|
261
|
+
return event?.label || `Run ${name}`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toolCallIdFromEvent(event) {
|
|
265
|
+
return String(event?.toolCallId || event?.id || '')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function toolInput(event) {
|
|
269
|
+
if (event?.args !== undefined) return event.args
|
|
270
|
+
if (event?.input !== undefined) return event.input
|
|
271
|
+
if (event?.params !== undefined) return event.params
|
|
272
|
+
return undefined
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function toolOutput(event) {
|
|
276
|
+
if (event?.result !== undefined) return event.result
|
|
277
|
+
if (event?.output !== undefined) return event.output
|
|
278
|
+
if (event?.error !== undefined) return { error: event.error }
|
|
279
|
+
return undefined
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toolContentFromOutput(output) {
|
|
283
|
+
if (output === undefined) return undefined
|
|
284
|
+
return [{ type: 'content', content: textContent(truncateText(output)) }]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function permissionKey(sessionId, toolCallId) {
|
|
288
|
+
return `${sessionId}:${toolCallId}`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function requestToolPermission(sessionId, event, conn) {
|
|
292
|
+
const toolCallId = toolCallIdFromEvent(event)
|
|
293
|
+
if (!toolCallId) return
|
|
294
|
+
const key = permissionKey(sessionId, toolCallId)
|
|
295
|
+
if (pendingPermissions.has(key)) return
|
|
296
|
+
pendingPermissions.add(key)
|
|
297
|
+
try {
|
|
298
|
+
const response = await conn.requestPermission({
|
|
299
|
+
sessionId,
|
|
300
|
+
toolCall: {
|
|
301
|
+
toolCallId,
|
|
302
|
+
kind: toolKind(event.toolName),
|
|
303
|
+
status: 'pending',
|
|
304
|
+
title: toolTitle(event),
|
|
305
|
+
rawInput: event.args,
|
|
306
|
+
},
|
|
307
|
+
options: [
|
|
308
|
+
{ optionId: 'allow_once', name: 'Allow once', kind: 'allow_once' },
|
|
309
|
+
{ optionId: 'reject_once', name: 'Reject', kind: 'reject_once' },
|
|
310
|
+
],
|
|
311
|
+
})
|
|
312
|
+
const selected = response?.outcome?.outcome === 'selected' ? response.outcome.optionId : null
|
|
313
|
+
if (selected === 'allow_once') {
|
|
314
|
+
approveToolCall(sessionId, toolCallId)
|
|
315
|
+
} else {
|
|
316
|
+
rejectToolCall(sessionId, toolCallId)
|
|
317
|
+
}
|
|
318
|
+
} finally {
|
|
319
|
+
pendingPermissions.delete(key)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function convertEventToUpdates(event, state = {}) {
|
|
324
|
+
if (!event || typeof event !== 'object') return []
|
|
325
|
+
const updates = []
|
|
326
|
+
|
|
327
|
+
if (event.type === 'message_start' || event.type === 'message_update' || event.type === 'message_end') {
|
|
328
|
+
const text = eventMessageText(event)
|
|
329
|
+
const messageId = String(event.message?.id || event.messageId || 'assistant')
|
|
330
|
+
const previous = state.messageTextById?.get(messageId) || ''
|
|
331
|
+
const chunk = text.startsWith(previous) ? text.slice(previous.length) : text
|
|
332
|
+
if (chunk && event.message?.role === 'assistant') {
|
|
333
|
+
state.messageTextById?.set(messageId, text)
|
|
334
|
+
updates.push({
|
|
335
|
+
sessionUpdate: 'agent_message_chunk',
|
|
336
|
+
content: textContent(chunk),
|
|
337
|
+
messageId,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (event.type === 'tool_execution_start') {
|
|
343
|
+
const id = toolCallIdFromEvent(event)
|
|
344
|
+
if (id) {
|
|
345
|
+
updates.push({
|
|
346
|
+
sessionUpdate: 'tool_call',
|
|
347
|
+
toolCallId: id,
|
|
348
|
+
title: toolTitle(event),
|
|
349
|
+
kind: toolKind(event.toolName),
|
|
350
|
+
status: 'in_progress',
|
|
351
|
+
rawInput: toolInput(event),
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (event.type === 'tool_execution_update') {
|
|
357
|
+
const id = toolCallIdFromEvent(event)
|
|
358
|
+
if (id) {
|
|
359
|
+
updates.push({
|
|
360
|
+
sessionUpdate: 'tool_call_update',
|
|
361
|
+
toolCallId: id,
|
|
362
|
+
status: 'in_progress',
|
|
363
|
+
rawOutput: toolOutput(event),
|
|
364
|
+
content: toolContentFromOutput(toolOutput(event)),
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (event.type === 'tool_execution_end') {
|
|
370
|
+
const id = toolCallIdFromEvent(event)
|
|
371
|
+
if (id) {
|
|
372
|
+
const output = toolOutput(event)
|
|
373
|
+
updates.push({
|
|
374
|
+
sessionUpdate: 'tool_call_update',
|
|
375
|
+
toolCallId: id,
|
|
376
|
+
status: event.error ? 'failed' : 'completed',
|
|
377
|
+
rawOutput: output,
|
|
378
|
+
content: toolContentFromOutput(output),
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (event.type === 'tool_approval_required') {
|
|
384
|
+
const id = toolCallIdFromEvent(event)
|
|
385
|
+
if (id) {
|
|
386
|
+
updates.push({
|
|
387
|
+
sessionUpdate: 'tool_call',
|
|
388
|
+
toolCallId: id,
|
|
389
|
+
title: toolTitle(event),
|
|
390
|
+
kind: toolKind(event.toolName),
|
|
391
|
+
status: 'pending',
|
|
392
|
+
rawInput: event.args,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (event.type === 'title_updated' && typeof event.title === 'string') {
|
|
398
|
+
updates.push({
|
|
399
|
+
sessionUpdate: 'session_info_update',
|
|
400
|
+
title: event.title,
|
|
401
|
+
updatedAt: new Date().toISOString(),
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (event.contextUsage?.used && event.contextUsage?.size) {
|
|
406
|
+
updates.push({
|
|
407
|
+
sessionUpdate: 'usage_update',
|
|
408
|
+
used: event.contextUsage.used,
|
|
409
|
+
size: event.contextUsage.size,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return updates
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function sessionModes() {
|
|
417
|
+
return {
|
|
418
|
+
currentModeId: DEFAULT_MODE_ID,
|
|
419
|
+
availableModes: [{
|
|
420
|
+
id: DEFAULT_MODE_ID,
|
|
421
|
+
name: 'Default',
|
|
422
|
+
description: 'QuickForge default agent mode',
|
|
423
|
+
}],
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function parseStoredJson(value) {
|
|
428
|
+
if (typeof value !== 'string') return value
|
|
429
|
+
try { return JSON.parse(value) } catch { return null }
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isUsableModel(model) {
|
|
433
|
+
return Boolean(model?.id && model?.provider && model?.api && model?.baseUrl)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function sameBaseUrl(a, b) {
|
|
437
|
+
return String(a || '').trim().replace(/\/$/, '') === String(b || '').trim().replace(/\/$/, '')
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function sameModel(a, b) {
|
|
441
|
+
return Boolean(a && b && a.id === b.id && a.provider === b.provider && a.api === b.api && sameBaseUrl(a.baseUrl, b.baseUrl))
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function modelValueId(model) {
|
|
445
|
+
const payload = JSON.stringify({
|
|
446
|
+
id: model.id,
|
|
447
|
+
provider: model.provider,
|
|
448
|
+
api: model.api,
|
|
449
|
+
baseUrl: model.baseUrl,
|
|
450
|
+
})
|
|
451
|
+
return Buffer.from(payload, 'utf8').toString('base64url')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function modelDisplayName(model) {
|
|
455
|
+
return model.name || `${model.provider}/${model.id}`
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isThinkingLevel(value) {
|
|
459
|
+
return THINKING_LEVELS.some((level) => level.value === value)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function thinkingLevelConfigOption(currentModel = null, currentThinkingLevel = 'off') {
|
|
463
|
+
const supportsThinking = currentModel?.reasoning === true
|
|
464
|
+
const options = supportsThinking ? THINKING_LEVELS : THINKING_LEVELS.slice(0, 1)
|
|
465
|
+
const currentValue = supportsThinking && isThinkingLevel(currentThinkingLevel) ? currentThinkingLevel : 'off'
|
|
466
|
+
return {
|
|
467
|
+
id: THINKING_LEVEL_CONFIG_ID,
|
|
468
|
+
name: 'Thinking Level',
|
|
469
|
+
description: supportsThinking
|
|
470
|
+
? 'Select the reasoning/thinking level for this ACP session.'
|
|
471
|
+
: 'The selected model does not support reasoning, so thinking is disabled.',
|
|
472
|
+
category: 'thought_level',
|
|
473
|
+
type: 'select',
|
|
474
|
+
currentValue,
|
|
475
|
+
options,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function readConfiguredModels() {
|
|
480
|
+
const store = await readStore('custom-providers').catch(() => [])
|
|
481
|
+
const providers = Array.isArray(store) ? store : Object.values(store || {})
|
|
482
|
+
return providers
|
|
483
|
+
.flatMap((provider) => Array.isArray(provider?.models) ? provider.models : [])
|
|
484
|
+
.filter(isUsableModel)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function readActiveModel() {
|
|
488
|
+
const settings = await readStore('settings').catch(() => ({}))
|
|
489
|
+
const model = parseStoredJson(settings?.['active-model'])
|
|
490
|
+
return isUsableModel(model) ? model : null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function resolveInitialModel() {
|
|
494
|
+
const [configuredModels, activeModel] = await Promise.all([readConfiguredModels(), readActiveModel()])
|
|
495
|
+
if (activeModel) {
|
|
496
|
+
return configuredModels.find((model) => sameModel(model, activeModel)) || activeModel
|
|
497
|
+
}
|
|
498
|
+
return configuredModels[0] || null
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Resolve the initial thinking level for a new ACP session, mirroring the web UI
|
|
502
|
+
// (src/hooks/useAgentManager.ts): prefer the user's saved default thinking level,
|
|
503
|
+
// otherwise fall back to 'medium' for reasoning models and 'off' otherwise.
|
|
504
|
+
async function resolveInitialThinkingLevel(model) {
|
|
505
|
+
const settings = await readStore('settings').catch(() => ({}))
|
|
506
|
+
const defaultOptions = parseStoredJson(settings?.['default-options'])
|
|
507
|
+
const saved = defaultOptions?.thinkingLevel
|
|
508
|
+
if (isThinkingLevel(saved)) return saved
|
|
509
|
+
return model?.reasoning === true ? 'medium' : 'off'
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function sessionConfigOptions(currentModel = null, currentThinkingLevel = 'off') {
|
|
513
|
+
const configuredModels = await readConfiguredModels()
|
|
514
|
+
const models = [...configuredModels]
|
|
515
|
+
const options = []
|
|
516
|
+
if (currentModel && !models.some((model) => sameModel(model, currentModel))) models.unshift(currentModel)
|
|
517
|
+
|
|
518
|
+
if (models.length > 0) {
|
|
519
|
+
const selectedModel = currentModel || models[0]
|
|
520
|
+
const groups = []
|
|
521
|
+
for (const model of models) {
|
|
522
|
+
const groupId = model.provider || 'custom'
|
|
523
|
+
let group = groups.find((item) => item.group === groupId)
|
|
524
|
+
if (!group) {
|
|
525
|
+
group = { group: groupId, name: model.provider || 'Custom', options: [] }
|
|
526
|
+
groups.push(group)
|
|
527
|
+
}
|
|
528
|
+
group.options.push({
|
|
529
|
+
value: modelValueId(model),
|
|
530
|
+
name: modelDisplayName(model),
|
|
531
|
+
description: `${model.provider} · ${model.api} · ${model.baseUrl}`,
|
|
532
|
+
_meta: { quickforgeModel: model },
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
options.push({
|
|
537
|
+
id: MODEL_CONFIG_ID,
|
|
538
|
+
name: 'Model',
|
|
539
|
+
description: 'Select the QuickForge model used by this ACP session.',
|
|
540
|
+
category: 'model',
|
|
541
|
+
type: 'select',
|
|
542
|
+
currentValue: modelValueId(selectedModel),
|
|
543
|
+
options: groups,
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
options.push(thinkingLevelConfigOption(currentModel, currentThinkingLevel))
|
|
548
|
+
return options
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function sessionConfigOptionsForSession(sessionId) {
|
|
552
|
+
const state = getSessionState(sessionId)
|
|
553
|
+
if (!state) throw new Error('Session not found')
|
|
554
|
+
return sessionConfigOptions(state.model, state.thinkingLevel)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function selectSessionModel(sessionId, value) {
|
|
558
|
+
const state = getSessionState(sessionId)
|
|
559
|
+
if (!state) throw new Error('Session not found')
|
|
560
|
+
const models = await readConfiguredModels()
|
|
561
|
+
if (state.model && !models.some((model) => sameModel(model, state.model))) models.unshift(state.model)
|
|
562
|
+
const model = models.find((candidate) => modelValueId(candidate) === value)
|
|
563
|
+
if (!model) throw new Error('Selected model is not configured in QuickForge.')
|
|
564
|
+
updateSessionModel(sessionId, model)
|
|
565
|
+
const thinkingLevel = model.reasoning === true ? state.thinkingLevel : 'off'
|
|
566
|
+
if (thinkingLevel !== state.thinkingLevel) updateSessionThinkingLevel(sessionId, thinkingLevel)
|
|
567
|
+
return { configOptions: await sessionConfigOptions(model, thinkingLevel) }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function selectSessionThinkingLevel(sessionId, value) {
|
|
571
|
+
const state = getSessionState(sessionId)
|
|
572
|
+
if (!state) throw new Error('Session not found')
|
|
573
|
+
if (!isThinkingLevel(value)) throw new Error(`Unknown thinking level: ${value}`)
|
|
574
|
+
if (value !== 'off' && state.model?.reasoning !== true) throw new Error('The selected model does not support reasoning.')
|
|
575
|
+
updateSessionThinkingLevel(sessionId, value)
|
|
576
|
+
return { configOptions: await sessionConfigOptions(state.model, value) }
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function assertSafeAcpCwd(cwd) {
|
|
580
|
+
if (typeof cwd !== 'string' || !cwd.trim()) {
|
|
581
|
+
throw new Error('ACP session cwd is required.')
|
|
582
|
+
}
|
|
583
|
+
if (!path.isAbsolute(cwd)) {
|
|
584
|
+
throw new Error('ACP session cwd must be an absolute path.')
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const resolved = path.resolve(cwd)
|
|
588
|
+
const stat = await fs.stat(resolved).catch(() => null)
|
|
589
|
+
if (!stat?.isDirectory()) {
|
|
590
|
+
throw new Error(`ACP session cwd is not a directory: ${resolved}`)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const real = await fs.realpath(resolved)
|
|
594
|
+
const normalized = path.resolve(real).toLowerCase()
|
|
595
|
+
if (DANGEROUS_WORKSPACE_ROOTS.has(normalized)) {
|
|
596
|
+
throw new Error(`Refusing to use unsafe ACP workspace root: ${real}`)
|
|
597
|
+
}
|
|
598
|
+
return real
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function assertSafeAcpAdditionalDirectories(additionalDirectories = []) {
|
|
602
|
+
if (!Array.isArray(additionalDirectories)) return []
|
|
603
|
+
const result = []
|
|
604
|
+
const seen = new Set()
|
|
605
|
+
for (const dir of additionalDirectories) {
|
|
606
|
+
const safeDir = await assertSafeAcpCwd(dir)
|
|
607
|
+
const key = safeDir.toLowerCase()
|
|
608
|
+
if (!seen.has(key)) {
|
|
609
|
+
seen.add(key)
|
|
610
|
+
result.push(safeDir)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return result
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function isDefaultWorkspaceCwd(cwd) {
|
|
617
|
+
const defaultWorkspaceRoot = getDefaultWorkspaceRoot()
|
|
618
|
+
return defaultWorkspaceRoot && sameProjectPath(cwd, defaultWorkspaceRoot)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function resolveProjectForCwd(cwd) {
|
|
622
|
+
const resolvedCwd = await assertSafeAcpCwd(cwd)
|
|
623
|
+
if (isDefaultWorkspaceCwd(resolvedCwd)) return null
|
|
624
|
+
const config = await readProjectConfig()
|
|
625
|
+
let project = config.projects.find((item) => sameProjectPath(item.path, resolvedCwd))
|
|
626
|
+
if (!project) {
|
|
627
|
+
const updated = await setActiveProjectPath(resolvedCwd)
|
|
628
|
+
project = updated.project
|
|
629
|
+
}
|
|
630
|
+
return project || getActiveProject(config)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function createQuickForgeSession(params = {}) {
|
|
634
|
+
const cwd = await assertSafeAcpCwd(params.cwd)
|
|
635
|
+
const additionalDirectories = await assertSafeAcpAdditionalDirectories(params.additionalDirectories)
|
|
636
|
+
const project = await resolveProjectForCwd(cwd)
|
|
637
|
+
const sessionId = params._meta?.quickforgeSessionId || randomUUID()
|
|
638
|
+
const model = await resolveInitialModel()
|
|
639
|
+
const thinkingLevel = await resolveInitialThinkingLevel(model)
|
|
640
|
+
await createAgent(sessionId, {
|
|
641
|
+
scope: project?.id ? 'project' : 'global',
|
|
642
|
+
projectId: project?.id || null,
|
|
643
|
+
accessMode: 'default',
|
|
644
|
+
yoloMode: false,
|
|
645
|
+
model,
|
|
646
|
+
thinkingLevel,
|
|
647
|
+
title: 'ACP session',
|
|
648
|
+
idleRetention: 'always',
|
|
649
|
+
})
|
|
650
|
+
acpSessions.set(sessionId, { cwd, additionalDirectories, projectId: project?.id || null })
|
|
651
|
+
return { sessionId, modes: sessionModes(), configOptions: await sessionConfigOptions(model, thinkingLevel) }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function waitForPromptEnd(sessionId, conn, signal) {
|
|
655
|
+
if (pendingPrompts.has(sessionId)) return Promise.reject(new Error('ACP prompt is already running for this session.'))
|
|
656
|
+
const eventBus = getSessionEventBus(sessionId)
|
|
657
|
+
if (!eventBus) return Promise.reject(new Error('Session not found'))
|
|
658
|
+
|
|
659
|
+
return new Promise((resolve, reject) => {
|
|
660
|
+
let settled = false
|
|
661
|
+
|
|
662
|
+
const cleanup = () => {
|
|
663
|
+
pendingPrompts.delete(sessionId)
|
|
664
|
+
eventBus.off('agent_event', onEvent)
|
|
665
|
+
signal?.removeEventListener?.('abort', onAbort)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const finish = (result) => {
|
|
669
|
+
if (settled) return
|
|
670
|
+
settled = true
|
|
671
|
+
cleanup()
|
|
672
|
+
resolve(result)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const fail = (err) => {
|
|
676
|
+
if (settled) return
|
|
677
|
+
settled = true
|
|
678
|
+
cleanup()
|
|
679
|
+
reject(err)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const sendUpdate = (update) => {
|
|
683
|
+
conn.sessionUpdate({ sessionId, update }).catch((err) => {
|
|
684
|
+
logger.warn(`ACP session/update failed for ${sessionId}: ${err?.message || err}`, { sessionId })
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const conversionState = { messageTextById: new Map() }
|
|
689
|
+
|
|
690
|
+
const onEvent = (event) => {
|
|
691
|
+
for (const update of convertEventToUpdates(event, conversionState)) sendUpdate(update)
|
|
692
|
+
|
|
693
|
+
if (event?.type === 'tool_approval_required') {
|
|
694
|
+
requestToolPermission(sessionId, event, conn).catch((err) => {
|
|
695
|
+
logger.warn(`ACP tool permission failed for ${sessionId}: ${err?.message || err}`, { sessionId })
|
|
696
|
+
try { rejectToolCall(sessionId, event.toolCallId) } catch { /* ignore */ }
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (event?.type === 'error') {
|
|
701
|
+
fail(new Error(event.error || 'QuickForge agent error'))
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (event?.type === 'agent_end') {
|
|
706
|
+
finish({ stopReason: event.status === 'aborted' ? 'cancelled' : 'end_turn' })
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const onAbort = () => {
|
|
711
|
+
abortRun(sessionId).catch(() => {})
|
|
712
|
+
finish({ stopReason: 'cancelled' })
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
pendingPrompts.set(sessionId, { cancel: onAbort })
|
|
716
|
+
|
|
717
|
+
eventBus.on('agent_event', onEvent)
|
|
718
|
+
if (signal) {
|
|
719
|
+
if (signal.aborted) {
|
|
720
|
+
onAbort()
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
724
|
+
}
|
|
725
|
+
})
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function listPersistedAcpSessions(params = {}) {
|
|
729
|
+
const [metadata, projectConfig] = await Promise.all([
|
|
730
|
+
readStore('sessions-metadata').catch(() => ({})),
|
|
731
|
+
readProjectConfig().catch(() => ({ projects: [] })),
|
|
732
|
+
])
|
|
733
|
+
const projectPathById = new Map((projectConfig.projects || []).map((project) => [project.id, path.resolve(project.path)]))
|
|
734
|
+
const requestedCwd = params.cwd ? path.resolve(params.cwd) : null
|
|
735
|
+
const sessionsById = new Map()
|
|
736
|
+
|
|
737
|
+
for (const meta of Object.values(metadata || {})) {
|
|
738
|
+
if (!meta?.id) continue
|
|
739
|
+
const cwd = meta.scope === 'project' && meta.projectId && projectPathById.has(meta.projectId)
|
|
740
|
+
? projectPathById.get(meta.projectId)
|
|
741
|
+
: process.cwd()
|
|
742
|
+
if (requestedCwd && path.resolve(cwd) !== requestedCwd) continue
|
|
743
|
+
sessionsById.set(meta.id, {
|
|
744
|
+
sessionId: meta.id,
|
|
745
|
+
cwd,
|
|
746
|
+
additionalDirectories: [],
|
|
747
|
+
title: meta.title || 'ACP session',
|
|
748
|
+
updatedAt: meta.lastModified || meta.createdAt || new Date().toISOString(),
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (const session of listAgentSessions()) {
|
|
753
|
+
const context = acpSessions.get(session.sessionId)
|
|
754
|
+
const cwd = context?.cwd || process.cwd()
|
|
755
|
+
if (requestedCwd && path.resolve(cwd) !== requestedCwd) continue
|
|
756
|
+
sessionsById.set(session.sessionId, {
|
|
757
|
+
sessionId: session.sessionId,
|
|
758
|
+
cwd,
|
|
759
|
+
additionalDirectories: context?.additionalDirectories || [],
|
|
760
|
+
title: session.title || sessionsById.get(session.sessionId)?.title || 'ACP session',
|
|
761
|
+
updatedAt: new Date().toISOString(),
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return [...sessionsById.values()].sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export async function createQuickForgeAcpAgent() {
|
|
769
|
+
const pkg = await readPackageInfo()
|
|
770
|
+
return {
|
|
771
|
+
async initialize(params = {}) {
|
|
772
|
+
return {
|
|
773
|
+
protocolVersion: Math.min(params.protocolVersion || PROTOCOL_VERSION, PROTOCOL_VERSION),
|
|
774
|
+
agentCapabilities: {
|
|
775
|
+
promptCapabilities: {
|
|
776
|
+
embeddedContext: true,
|
|
777
|
+
},
|
|
778
|
+
sessionCapabilities: {
|
|
779
|
+
list: {},
|
|
780
|
+
delete: {},
|
|
781
|
+
close: {},
|
|
782
|
+
additionalDirectories: {},
|
|
783
|
+
},
|
|
784
|
+
nes: {
|
|
785
|
+
events: {
|
|
786
|
+
document: {
|
|
787
|
+
didOpen: {},
|
|
788
|
+
didChange: { syncKind: 'full' },
|
|
789
|
+
didClose: {},
|
|
790
|
+
didSave: {},
|
|
791
|
+
didFocus: {},
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
authMethods: [],
|
|
797
|
+
agentInfo: {
|
|
798
|
+
name: APP_NAME,
|
|
799
|
+
version: pkg.version || '0.0.0',
|
|
800
|
+
},
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
async newSession(params = {}) {
|
|
805
|
+
return createQuickForgeSession(params)
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
async loadSession(params = {}, conn = null) {
|
|
809
|
+
const additionalDirectories = await assertSafeAcpAdditionalDirectories(params.additionalDirectories)
|
|
810
|
+
const session = await restoreAgent(params.sessionId)
|
|
811
|
+
if (!session) throw new Error('Session not found')
|
|
812
|
+
const projectConfig = await readProjectConfig().catch(() => ({ projects: [] }))
|
|
813
|
+
const project = session.projectId ? projectConfig.projects.find((item) => item.id === session.projectId) : null
|
|
814
|
+
acpSessions.set(params.sessionId, {
|
|
815
|
+
cwd: params.cwd ? await assertSafeAcpCwd(params.cwd) : path.resolve(project?.path || process.cwd()),
|
|
816
|
+
additionalDirectories,
|
|
817
|
+
projectId: session.projectId || null,
|
|
818
|
+
})
|
|
819
|
+
await replaySessionHistory(params.sessionId, conn)
|
|
820
|
+
return { modes: sessionModes(), configOptions: await sessionConfigOptionsForSession(params.sessionId) }
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
async listSessions(params = {}) {
|
|
824
|
+
return { sessions: await listPersistedAcpSessions(params) }
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
async deleteSession(params = {}) {
|
|
828
|
+
await destroyAgent(params.sessionId)
|
|
829
|
+
acpSessions.delete(params.sessionId)
|
|
830
|
+
return {}
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
async closeSession(params = {}) {
|
|
834
|
+
try {
|
|
835
|
+
await abortRun(params.sessionId)
|
|
836
|
+
} catch {
|
|
837
|
+
// ignore idle or missing active run during close
|
|
838
|
+
}
|
|
839
|
+
await destroyAgent(params.sessionId)
|
|
840
|
+
acpSessions.delete(params.sessionId)
|
|
841
|
+
return {}
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
async setSessionConfigOption(params = {}) {
|
|
845
|
+
if (params.type === 'boolean') throw new Error('QuickForge ACP config options require select values.')
|
|
846
|
+
if (params.configId === MODEL_CONFIG_ID) return selectSessionModel(params.sessionId, params.value)
|
|
847
|
+
if (params.configId === THINKING_LEVEL_CONFIG_ID) return selectSessionThinkingLevel(params.sessionId, params.value)
|
|
848
|
+
throw new Error(`Unknown ACP config option: ${params.configId}`)
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
async prompt(params = {}, conn, signal) {
|
|
852
|
+
const message = normalizePromptText(params.prompt)
|
|
853
|
+
if (!message) throw new Error('Prompt is empty or unsupported.')
|
|
854
|
+
const state = getSessionState(params.sessionId)
|
|
855
|
+
if (!state) throw new Error('Session not found')
|
|
856
|
+
const done = waitForPromptEnd(params.sessionId, conn, signal)
|
|
857
|
+
await runPrompt(params.sessionId, withAcpContext(params.sessionId, message))
|
|
858
|
+
return done
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
async cancel(params = {}) {
|
|
862
|
+
const pendingPrompt = pendingPrompts.get(params.sessionId)
|
|
863
|
+
if (pendingPrompt) {
|
|
864
|
+
pendingPrompt.cancel()
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
await abortRun(params.sessionId)
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
async didOpenDocument(params = {}) {
|
|
871
|
+
updateAcpDocument(params, 'open')
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
async didChangeDocument(params = {}) {
|
|
875
|
+
updateAcpDocument(params, 'change')
|
|
876
|
+
},
|
|
877
|
+
|
|
878
|
+
async didSaveDocument(params = {}) {
|
|
879
|
+
updateAcpDocument(params, 'save')
|
|
880
|
+
},
|
|
881
|
+
|
|
882
|
+
async didCloseDocument(params = {}) {
|
|
883
|
+
closeAcpDocument(params)
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
async didFocusDocument(params = {}) {
|
|
887
|
+
focusAcpDocument(params)
|
|
888
|
+
},
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export async function runQuickForgeAcpStdio() {
|
|
893
|
+
const originalConsoleLog = console.log
|
|
894
|
+
console.log = (...args) => console.error(...args)
|
|
895
|
+
try {
|
|
896
|
+
const quickForgeAgent = await createQuickForgeAcpAgent()
|
|
897
|
+
const stream = ndJsonStream(
|
|
898
|
+
Writable.toWeb(process.stdout),
|
|
899
|
+
Readable.toWeb(process.stdin),
|
|
900
|
+
)
|
|
901
|
+
const connection = new AgentSideConnection((conn) => ({
|
|
902
|
+
initialize: (params) => quickForgeAgent.initialize(params),
|
|
903
|
+
newSession: (params) => quickForgeAgent.newSession(params),
|
|
904
|
+
loadSession: (params) => quickForgeAgent.loadSession(params, conn),
|
|
905
|
+
listSessions: (params) => quickForgeAgent.listSessions(params),
|
|
906
|
+
deleteSession: (params) => quickForgeAgent.deleteSession(params),
|
|
907
|
+
closeSession: (params) => quickForgeAgent.closeSession(params),
|
|
908
|
+
setSessionConfigOption: (params) => quickForgeAgent.setSessionConfigOption(params),
|
|
909
|
+
prompt: (params) => quickForgeAgent.prompt(params, conn, connection.signal),
|
|
910
|
+
cancel: (params) => quickForgeAgent.cancel(params),
|
|
911
|
+
unstable_didOpenDocument: (params) => quickForgeAgent.didOpenDocument(params),
|
|
912
|
+
unstable_didChangeDocument: (params) => quickForgeAgent.didChangeDocument(params),
|
|
913
|
+
unstable_didSaveDocument: (params) => quickForgeAgent.didSaveDocument(params),
|
|
914
|
+
unstable_didCloseDocument: (params) => quickForgeAgent.didCloseDocument(params),
|
|
915
|
+
unstable_didFocusDocument: (params) => quickForgeAgent.didFocusDocument(params),
|
|
916
|
+
}), stream)
|
|
917
|
+
await connection.closed
|
|
918
|
+
} finally {
|
|
919
|
+
console.log = originalConsoleLog
|
|
920
|
+
}
|
|
921
|
+
}
|