@shawnstack/quickforge 1.6.13 → 1.7.1
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/dist/assets/AgentProfilesPage-__uY0AvK.js +1 -0
- package/dist/assets/{ChatPanelHost-CxEotTsy.js → ChatPanelHost-DIs_vWFX.js} +1 -1
- package/dist/assets/{PluginsPage-DZlqGFLn.js → PluginsPage-CKyWhlCo.js} +1 -1
- package/dist/assets/ScheduledTasksPage-KtJoeJYt.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-BJq11OPD.js → SettingsWorkspacePage-DwqEnUmX.js} +362 -349
- package/dist/assets/{SharedConversationPage-BCR8xzii.js → SharedConversationPage-yujCRsbe.js} +1 -1
- package/dist/assets/TerminalDock-B9xKnimU.js +2 -0
- package/dist/assets/WorkspaceInspector-CE6RD6Ys.js +13 -0
- package/dist/assets/icons-pPRMD2tE.js +1 -0
- package/dist/assets/index-BSXpUDCq.js +63 -0
- package/dist/assets/index-DpO7jEGP.css +3 -0
- package/dist/assets/{mcp-servers-dialog-BHljnwYv.js → mcp-servers-dialog-CaQTvGiW.js} +2 -2
- package/dist/assets/{monaco-DEQoYzYI.js → monaco-CPwJMUsl.js} +1 -1
- package/dist/assets/{react-vendor-CZsiwuxm.js → react-vendor-CLbWF1Oy.js} +1 -1
- package/dist/assets/{skills-dialog-8rn1XU9U.js → skills-dialog-D18mxNyW.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/server/agent-manager.mjs +19 -14
- package/server/auto-archive.mjs +137 -0
- package/server/auto-compaction.mjs +15 -18
- package/server/conversation-compaction.mjs +69 -3
- package/server/index.mjs +6 -1
- package/server/routes/backup.mjs +79 -72
- package/server/routes/storage.mjs +4 -0
- package/server/routes/workspace.mjs +84 -32
- package/server/session-persistence-lock.mjs +9 -0
- package/server/storage.mjs +11 -0
- package/dist/assets/AgentProfilesPage-Be0oZDW5.js +0 -1
- package/dist/assets/ScheduledTasksPage-DTkj-0O4.js +0 -2
- package/dist/assets/TerminalDock-Bs5b-YNp.js +0 -2
- package/dist/assets/WorkspaceInspector-BP0XyAlE.js +0 -13
- package/dist/assets/icons-DAxUA0e-.js +0 -1
- package/dist/assets/index-BZC61KZg.css +0 -3
- package/dist/assets/index-DGQbvw7v.js +0 -63
|
@@ -6,6 +6,16 @@ import { cacheDir } from './storage.mjs'
|
|
|
6
6
|
export const DEFAULT_COMPACT_KEEP_TURNS = 0
|
|
7
7
|
const MAX_COMPACT_KEEP_TURNS = 20
|
|
8
8
|
const MIN_SUMMARY_SOURCE_CHARS = 1600
|
|
9
|
+
const COMPACT_SUMMARY_OPEN_TAG = '<compact_summary>'
|
|
10
|
+
const COMPACT_SUMMARY_CLOSE_TAG = '</compact_summary>'
|
|
11
|
+
const COMPACTION_DETAILS_KEY = 'quickforgeCompaction'
|
|
12
|
+
const LEGACY_COMPACT_SUMMARY_INTROS = [
|
|
13
|
+
'The previous conversation has been compacted.',
|
|
14
|
+
'The previous conversation has been automatically compacted.',
|
|
15
|
+
'Existing rolling compact summary from earlier conversation history:',
|
|
16
|
+
]
|
|
17
|
+
const LEGACY_COMPACTION_NOTICE_PREFIX = '已基于当前对话创建压缩后的新对话:'
|
|
18
|
+
const LEGACY_COMPACTION_NOTICE_SUFFIX = '压缩前历史已保存到本地备份。'
|
|
9
19
|
|
|
10
20
|
export const COMPACT_SYSTEM_PROMPT = `你是 QuickForge 的“历史对话压缩器”。你的任务是把一段较长的 AI 助手对话压缩成后续模型继续工作所需的最小充分上下文。
|
|
11
21
|
|
|
@@ -72,8 +82,60 @@ function normalizeKeepTurns(value) {
|
|
|
72
82
|
return Math.min(MAX_COMPACT_KEEP_TURNS, Math.max(0, Math.floor(parsed)))
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
function messageContentText(message) {
|
|
86
|
+
const content = message?.content
|
|
87
|
+
if (typeof content === 'string') return content
|
|
88
|
+
if (!Array.isArray(content)) return ''
|
|
89
|
+
return content
|
|
90
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
91
|
+
.map((block) => block.text)
|
|
92
|
+
.join('\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compactionMessageKind(message) {
|
|
96
|
+
const kind = message?.details?.[COMPACTION_DETAILS_KEY]?.kind
|
|
97
|
+
if (kind === 'summary' || kind === 'notice') return kind
|
|
98
|
+
|
|
99
|
+
const text = messageContentText(message).trim()
|
|
100
|
+
if (
|
|
101
|
+
message?.role === 'user'
|
|
102
|
+
&& LEGACY_COMPACT_SUMMARY_INTROS.some((intro) => text.startsWith(intro))
|
|
103
|
+
&& text.includes(COMPACT_SUMMARY_OPEN_TAG)
|
|
104
|
+
&& text.lastIndexOf(COMPACT_SUMMARY_CLOSE_TAG) > text.indexOf(COMPACT_SUMMARY_OPEN_TAG)
|
|
105
|
+
) return 'summary'
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
message?.role === 'assistant'
|
|
109
|
+
&& text.startsWith(LEGACY_COMPACTION_NOTICE_PREFIX)
|
|
110
|
+
&& text.includes(LEGACY_COMPACTION_NOTICE_SUFFIX)
|
|
111
|
+
) return 'notice'
|
|
112
|
+
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function compactionMessageDetails(kind) {
|
|
117
|
+
return { [COMPACTION_DETAILS_KEY]: { version: 1, kind } }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isCompactSummaryMessage(message) {
|
|
121
|
+
return compactionMessageKind(message) === 'summary'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isCompactionNoticeMessage(message) {
|
|
125
|
+
return compactionMessageKind(message) === 'notice'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function extractCompactSummaryText(message) {
|
|
129
|
+
const text = messageContentText(message).trim()
|
|
130
|
+
const openIndex = text.indexOf(COMPACT_SUMMARY_OPEN_TAG)
|
|
131
|
+
const closeIndex = text.lastIndexOf(COMPACT_SUMMARY_CLOSE_TAG)
|
|
132
|
+
if (openIndex < 0 || closeIndex <= openIndex) return text
|
|
133
|
+
return text.slice(openIndex + COMPACT_SUMMARY_OPEN_TAG.length, closeIndex).trim()
|
|
134
|
+
}
|
|
135
|
+
|
|
75
136
|
function isUserMessage(message) {
|
|
76
|
-
return message?.role === 'user' || message?.role === 'user-with-attachments'
|
|
137
|
+
return (message?.role === 'user' || message?.role === 'user-with-attachments')
|
|
138
|
+
&& !isCompactSummaryMessage(message)
|
|
77
139
|
}
|
|
78
140
|
|
|
79
141
|
function truncateMiddle(value, maxLength) {
|
|
@@ -200,7 +262,7 @@ export function splitMessagesForCompaction(messages, options = {}) {
|
|
|
200
262
|
if (keepTurns <= 0) {
|
|
201
263
|
return {
|
|
202
264
|
keepTurns,
|
|
203
|
-
compactRange: sourceMessages.
|
|
265
|
+
compactRange: sourceMessages.filter((message) => !isCompactionNoticeMessage(message)),
|
|
204
266
|
recentTail: [],
|
|
205
267
|
tailStart: sourceMessages.length,
|
|
206
268
|
}
|
|
@@ -219,9 +281,13 @@ export function splitMessagesForCompaction(messages, options = {}) {
|
|
|
219
281
|
|
|
220
282
|
if (seenUserTurns < keepTurns) tailStart = 0
|
|
221
283
|
|
|
284
|
+
const compactRange = sourceMessages
|
|
285
|
+
.slice(0, tailStart)
|
|
286
|
+
.filter((message) => !isCompactionNoticeMessage(message))
|
|
287
|
+
|
|
222
288
|
return {
|
|
223
289
|
keepTurns,
|
|
224
|
-
compactRange
|
|
290
|
+
compactRange,
|
|
225
291
|
recentTail: sourceMessages.slice(tailStart),
|
|
226
292
|
tailStart,
|
|
227
293
|
}
|
package/server/index.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { handleSkillsApi } from './routes/skills.mjs'
|
|
|
19
19
|
import { handleAgentApi } from './routes/agent.mjs'
|
|
20
20
|
import { handleAgentProfilesApi } from './routes/agent-profiles.mjs'
|
|
21
21
|
import { handleScheduledTasksApi, startScheduledTaskRunner, stopScheduledTaskRunner } from './routes/scheduled-tasks.mjs'
|
|
22
|
+
import { startAutoArchiveRunner, stopAutoArchiveRunner } from './auto-archive.mjs'
|
|
22
23
|
import { handleBackupApi } from './routes/backup.mjs'
|
|
23
24
|
import { handleSystemApi } from './routes/system.mjs'
|
|
24
25
|
import { handleSharesApi } from './routes/shares.mjs'
|
|
@@ -147,6 +148,7 @@ async function performRestart() {
|
|
|
147
148
|
logger.info(`Restart supervisor started (PID ${supervisorPid}).`)
|
|
148
149
|
|
|
149
150
|
stopScheduledTaskRunner()
|
|
151
|
+
stopAutoArchiveRunner()
|
|
150
152
|
stopVite()
|
|
151
153
|
await shutdownAgentManager()
|
|
152
154
|
await shutdownMcpConnections()
|
|
@@ -221,6 +223,7 @@ function spawnUpdateSupervisor(update) {
|
|
|
221
223
|
async function shutdownForUpdate() {
|
|
222
224
|
logger.info('Shutting down QuickForge for external updater.')
|
|
223
225
|
stopScheduledTaskRunner()
|
|
226
|
+
stopAutoArchiveRunner()
|
|
224
227
|
stopVite()
|
|
225
228
|
await shutdownAgentManager()
|
|
226
229
|
await shutdownMcpConnections()
|
|
@@ -370,7 +373,7 @@ async function handleApi(req, res, url) {
|
|
|
370
373
|
}
|
|
371
374
|
|
|
372
375
|
// Project workspace inspector routes
|
|
373
|
-
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path' || pathname.startsWith('/api/workspace/preview/')) {
|
|
376
|
+
if (pathname === '/api/workspace/tree' || pathname === '/api/workspace/file' || pathname === '/api/workspace/resolve-path' || pathname === '/api/workspace/open-external' || pathname.startsWith('/api/workspace/preview/')) {
|
|
374
377
|
await handleWorkspaceApi(req, res, url)
|
|
375
378
|
return
|
|
376
379
|
}
|
|
@@ -671,6 +674,7 @@ await resetStaleTaskStatuses()
|
|
|
671
674
|
await initializeActiveProject()
|
|
672
675
|
setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
|
|
673
676
|
startScheduledTaskRunner()
|
|
677
|
+
startAutoArchiveRunner()
|
|
674
678
|
|
|
675
679
|
server.on('error', (error) => {
|
|
676
680
|
// Handle listen errors (most commonly EADDRINUSE). Without this, Node would
|
|
@@ -709,6 +713,7 @@ server.listen(port, host, () => {
|
|
|
709
713
|
async function gracefulShutdown(signal) {
|
|
710
714
|
logger.info(`Received ${signal}, shutting down gracefully...`)
|
|
711
715
|
stopScheduledTaskRunner()
|
|
716
|
+
stopAutoArchiveRunner()
|
|
712
717
|
stopVite()
|
|
713
718
|
await shutdownAgentManager()
|
|
714
719
|
await shutdownMcpConnections()
|
package/server/routes/backup.mjs
CHANGED
|
@@ -7,18 +7,14 @@ import {
|
|
|
7
7
|
readStore,
|
|
8
8
|
writeStore,
|
|
9
9
|
readProjectConfigData,
|
|
10
|
-
writeProjectConfigData,
|
|
11
10
|
storageDir,
|
|
12
11
|
} from '../storage.mjs'
|
|
13
|
-
import { initializeActiveProject } from '../project-config.mjs'
|
|
14
|
-
import { setActiveWorkspaceRootForFilesystem } from './filesystem.mjs'
|
|
15
|
-
import { getWorkspaceRoot } from '../utils/workspace.mjs'
|
|
16
12
|
|
|
17
13
|
const BACKUP_VERSION = 1
|
|
18
14
|
const BACKUP_APP = 'quickforge'
|
|
19
15
|
const IMPORT_UPLOAD_MAX_BYTES = Number(process.env.QUICKFORGE_IMPORT_UPLOAD_MAX_BYTES || 1024 * 1024 * 1024)
|
|
20
16
|
const backupScopes = new Set(['all', 'config', 'sessions'])
|
|
21
|
-
const settingsSectionIds = ['settings', 'mcp', 'providerKeys', 'customProviders', '
|
|
17
|
+
const settingsSectionIds = ['settings', 'mcp', 'providerKeys', 'customProviders', 'scheduledTasks']
|
|
22
18
|
const exportSectionIds = new Set(settingsSectionIds)
|
|
23
19
|
const restoreSectionIds = new Set([...settingsSectionIds, 'conversations'])
|
|
24
20
|
const restoreModes = new Set(['replace', 'merge'])
|
|
@@ -138,21 +134,6 @@ function assertObjectSection(value, name) {
|
|
|
138
134
|
return value
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
function assertProjectConfig(value) {
|
|
142
|
-
const projectConfig = assertObjectSection(value, 'projects')
|
|
143
|
-
if (projectConfig === undefined) return undefined
|
|
144
|
-
if (!Array.isArray(projectConfig.projects)) {
|
|
145
|
-
const error = new Error('Invalid backup section: projects.projects must be an array')
|
|
146
|
-
error.statusCode = 400
|
|
147
|
-
throw error
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
activeProjectId: typeof projectConfig.activeProjectId === 'string' ? projectConfig.activeProjectId : null,
|
|
151
|
-
globalSkills: Array.isArray(projectConfig.globalSkills) ? projectConfig.globalSkills : [],
|
|
152
|
-
projects: projectConfig.projects,
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
137
|
function filterSessionsByMetadata(sessions, metadata) {
|
|
157
138
|
if (!sessions || !metadata) return sessions
|
|
158
139
|
const metadataIds = new Set(Object.keys(metadata))
|
|
@@ -226,8 +207,8 @@ async function buildBackup(scope = 'all', options = {}) {
|
|
|
226
207
|
shouldInclude('mcp') ? readStore('mcp').then((value) => ['mcp', value]) : null,
|
|
227
208
|
shouldInclude('providerKeys') ? readStore('provider-keys').then((value) => ['providerKeys', value]) : null,
|
|
228
209
|
shouldInclude('customProviders') ? readStore('custom-providers').then((value) => ['customProviders', value]) : null,
|
|
229
|
-
shouldInclude('projects') ? readProjectConfigData().then((value) => ['projects', value]) : null,
|
|
230
210
|
shouldInclude('scheduledTasks') ? readStore('scheduled-tasks').then((value) => ['scheduledTasks', value]) : null,
|
|
211
|
+
options.includeLocalProjects ? readProjectConfigData().then((value) => ['projects', value]) : null,
|
|
231
212
|
])
|
|
232
213
|
for (const entry of entries) {
|
|
233
214
|
if (entry) data[entry[0]] = entry[1]
|
|
@@ -264,7 +245,14 @@ function normalizeBackupPayload(payload) {
|
|
|
264
245
|
throw error
|
|
265
246
|
}
|
|
266
247
|
|
|
267
|
-
const
|
|
248
|
+
const hasEnvelope = backup.data && typeof backup.data === 'object' && !Array.isArray(backup.data)
|
|
249
|
+
if (hasEnvelope && Number.isInteger(backup.version) && backup.version > BACKUP_VERSION) {
|
|
250
|
+
const error = new Error(`Unsupported backup version: ${backup.version}`)
|
|
251
|
+
error.statusCode = 400
|
|
252
|
+
throw error
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const data = hasEnvelope
|
|
268
256
|
? backup.data
|
|
269
257
|
: backup
|
|
270
258
|
|
|
@@ -310,14 +298,13 @@ function validateSettingsImportBackup(payload) {
|
|
|
310
298
|
const normalized = normalizeBackupPayload(payload)
|
|
311
299
|
const validSections = {}
|
|
312
300
|
const invalidSections = {}
|
|
301
|
+
const ignoredProjects = normalized.sections.projects !== undefined
|
|
313
302
|
|
|
314
303
|
for (const id of settingsSectionIds) {
|
|
315
304
|
const value = normalized.sections[id]
|
|
316
305
|
if (value === undefined) continue
|
|
317
306
|
try {
|
|
318
|
-
validSections[id] = id
|
|
319
|
-
? assertProjectConfig(value)
|
|
320
|
-
: assertObjectSection(value, id)
|
|
307
|
+
validSections[id] = assertObjectSection(value, id)
|
|
321
308
|
} catch (error) {
|
|
322
309
|
invalidSections[id] = error instanceof Error ? error.message : `Invalid backup section: ${id}`
|
|
323
310
|
}
|
|
@@ -339,6 +326,7 @@ function validateSettingsImportBackup(payload) {
|
|
|
339
326
|
data: validSections,
|
|
340
327
|
},
|
|
341
328
|
invalidSections,
|
|
329
|
+
ignoredProjects,
|
|
342
330
|
}
|
|
343
331
|
}
|
|
344
332
|
|
|
@@ -351,18 +339,25 @@ function validateBackupPayload(payload) {
|
|
|
351
339
|
? normalizeSessionMetadata(sessions, sections.sessionsMetadata)
|
|
352
340
|
: assertObjectSection(sections.sessionsMetadata, 'sessionsMetadata')
|
|
353
341
|
|
|
342
|
+
const validatedSections = {
|
|
343
|
+
settings: assertObjectSection(sections.settings, 'settings'),
|
|
344
|
+
mcp: assertObjectSection(sections.mcp, 'mcp'),
|
|
345
|
+
providerKeys: assertObjectSection(sections.providerKeys, 'providerKeys'),
|
|
346
|
+
customProviders: assertObjectSection(sections.customProviders, 'customProviders'),
|
|
347
|
+
scheduledTasks: assertObjectSection(sections.scheduledTasks, 'scheduledTasks'),
|
|
348
|
+
sessions,
|
|
349
|
+
sessionsMetadata,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (Object.values(validatedSections).every((value) => value === undefined)) {
|
|
353
|
+
const error = new Error('Backup does not contain any restorable sections')
|
|
354
|
+
error.statusCode = 400
|
|
355
|
+
throw error
|
|
356
|
+
}
|
|
357
|
+
|
|
354
358
|
return {
|
|
355
359
|
...backup,
|
|
356
|
-
sections:
|
|
357
|
-
settings: assertObjectSection(sections.settings, 'settings'),
|
|
358
|
-
mcp: assertObjectSection(sections.mcp, 'mcp'),
|
|
359
|
-
providerKeys: assertObjectSection(sections.providerKeys, 'providerKeys'),
|
|
360
|
-
customProviders: assertObjectSection(sections.customProviders, 'customProviders'),
|
|
361
|
-
projects: assertProjectConfig(sections.projects),
|
|
362
|
-
scheduledTasks: assertObjectSection(sections.scheduledTasks, 'scheduledTasks'),
|
|
363
|
-
sessions,
|
|
364
|
-
sessionsMetadata,
|
|
365
|
-
},
|
|
360
|
+
sections: validatedSections,
|
|
366
361
|
}
|
|
367
362
|
}
|
|
368
363
|
|
|
@@ -411,7 +406,6 @@ function filterRestoreSections(sections, selected) {
|
|
|
411
406
|
mcp: selected.has('mcp') ? sections.mcp : undefined,
|
|
412
407
|
providerKeys: selected.has('providerKeys') ? sections.providerKeys : undefined,
|
|
413
408
|
customProviders: selected.has('customProviders') ? sections.customProviders : undefined,
|
|
414
|
-
projects: selected.has('projects') ? sections.projects : undefined,
|
|
415
409
|
scheduledTasks: selected.has('scheduledTasks') ? sections.scheduledTasks : undefined,
|
|
416
410
|
sessions: selected.has('conversations') ? sections.sessions : undefined,
|
|
417
411
|
sessionsMetadata: selected.has('conversations') ? sections.sessionsMetadata : undefined,
|
|
@@ -444,29 +438,67 @@ function countKeys(value) {
|
|
|
444
438
|
return value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value).length : 0
|
|
445
439
|
}
|
|
446
440
|
|
|
441
|
+
async function localProjectIds() {
|
|
442
|
+
const config = await readProjectConfigData()
|
|
443
|
+
return new Set(
|
|
444
|
+
(Array.isArray(config?.projects) ? config.projects : [])
|
|
445
|
+
.map((project) => project?.id)
|
|
446
|
+
.filter((id) => typeof id === 'string'),
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function scheduledTasksWithMissingProjects(tasks, projectIds) {
|
|
451
|
+
if (!tasks || typeof tasks !== 'object' || Array.isArray(tasks)) return []
|
|
452
|
+
return Object.values(tasks).filter((task) => (
|
|
453
|
+
task && typeof task === 'object' && !Array.isArray(task) &&
|
|
454
|
+
typeof task.projectId === 'string' && task.projectId && !projectIds.has(task.projectId)
|
|
455
|
+
))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function pauseScheduledTasksWithMissingProjects(tasks, projectIds) {
|
|
459
|
+
if (!tasks || typeof tasks !== 'object' || Array.isArray(tasks)) return tasks
|
|
460
|
+
return Object.fromEntries(Object.entries(tasks).map(([id, task]) => {
|
|
461
|
+
if (
|
|
462
|
+
task && typeof task === 'object' && !Array.isArray(task) &&
|
|
463
|
+
typeof task.projectId === 'string' && task.projectId && !projectIds.has(task.projectId)
|
|
464
|
+
) {
|
|
465
|
+
return [id, { ...task, status: 'paused' }]
|
|
466
|
+
}
|
|
467
|
+
return [id, task]
|
|
468
|
+
}))
|
|
469
|
+
}
|
|
470
|
+
|
|
447
471
|
function buildSummary(sections) {
|
|
448
472
|
const summary = {}
|
|
449
473
|
if (sections.settings !== undefined) summary.settings = countKeys(sections.settings)
|
|
450
474
|
if (sections.mcp !== undefined) summary.mcp = Array.isArray(sections.mcp?.mcpServers) ? sections.mcp.mcpServers.length : countKeys(sections.mcp)
|
|
451
475
|
if (sections.providerKeys !== undefined) summary.providerKeys = countKeys(sections.providerKeys)
|
|
452
476
|
if (sections.customProviders !== undefined) summary.customProviders = countKeys(sections.customProviders)
|
|
453
|
-
if (sections.projects !== undefined) summary.projects = sections.projects.projects.length
|
|
454
477
|
if (sections.scheduledTasks !== undefined) summary.scheduledTasks = countKeys(sections.scheduledTasks)
|
|
455
478
|
if (sections.sessions !== undefined) summary.sessions = countKeys(filterSessionsByMetadata(sections.sessions, sections.sessionsMetadata))
|
|
456
479
|
if (sections.sessionsMetadata !== undefined) summary.sessionsMetadata = countKeys(sections.sessionsMetadata)
|
|
457
480
|
return summary
|
|
458
481
|
}
|
|
459
482
|
|
|
460
|
-
function inspectBackup(payload) {
|
|
483
|
+
async function inspectBackup(payload) {
|
|
484
|
+
const normalized = normalizeBackupPayload(payload)
|
|
485
|
+
const ignoredProjects = normalized.sections.projects !== undefined
|
|
461
486
|
const backup = validateBackupPayload(payload)
|
|
462
487
|
const summary = buildSummary(backup.sections)
|
|
463
488
|
const warnings = []
|
|
464
489
|
const containsSecrets = countKeys(backup.sections.providerKeys) > 0
|
|
490
|
+
const missingProjectTasks = backup.sections.scheduledTasks === undefined
|
|
491
|
+
? []
|
|
492
|
+
: scheduledTasksWithMissingProjects(backup.sections.scheduledTasks, await localProjectIds())
|
|
465
493
|
|
|
466
494
|
if (containsSecrets) warnings.push('Backup contains API keys.')
|
|
495
|
+
if (ignoredProjects) warnings.push('Project lists and project-specific settings are local machine data and will not be imported.')
|
|
467
496
|
if (backup.sections.sessions !== undefined || backup.sections.sessionsMetadata !== undefined) {
|
|
468
497
|
warnings.push('Importing conversations will replace local conversation data.')
|
|
469
498
|
}
|
|
499
|
+
if (missingProjectTasks.length > 0) {
|
|
500
|
+
warnings.push(`${missingProjectTasks.length} scheduled task(s) reference projects that are not registered locally and will be paused after import.`)
|
|
501
|
+
}
|
|
470
502
|
|
|
471
503
|
return {
|
|
472
504
|
ok: true,
|
|
@@ -481,7 +513,7 @@ function inspectBackup(payload) {
|
|
|
481
513
|
}
|
|
482
514
|
|
|
483
515
|
async function writeSafetyBackup(scope = 'config') {
|
|
484
|
-
const backup = await buildBackup(scope, { includeSecrets: true })
|
|
516
|
+
const backup = await buildBackup(scope, { includeSecrets: true, includeLocalProjects: true })
|
|
485
517
|
const dir = path.join(storageDir, 'backups')
|
|
486
518
|
await fs.mkdir(dir, { recursive: true })
|
|
487
519
|
const file = path.join(dir, `quickforge-before-restore-${backupTimestamp()}.json`)
|
|
@@ -495,26 +527,6 @@ function mergeRecordStore(localValue, backupValue) {
|
|
|
495
527
|
return { ...(localValue && typeof localValue === 'object' ? localValue : {}), ...backupValue }
|
|
496
528
|
}
|
|
497
529
|
|
|
498
|
-
// Merge projects config: dedupe the projects array by id (backup wins on
|
|
499
|
-
// collision, local-only entries preserved), take activeProjectId / globalSkills
|
|
500
|
-
// from backup.
|
|
501
|
-
function mergeProjectConfig(localConfig, backupConfig) {
|
|
502
|
-
const localProjects = Array.isArray(localConfig?.projects) ? localConfig.projects : []
|
|
503
|
-
const backupProjects = Array.isArray(backupConfig.projects) ? backupConfig.projects : []
|
|
504
|
-
const merged = new Map()
|
|
505
|
-
for (const project of localProjects) {
|
|
506
|
-
if (project && typeof project.id === 'string') merged.set(project.id, project)
|
|
507
|
-
}
|
|
508
|
-
for (const project of backupProjects) {
|
|
509
|
-
if (project && typeof project.id === 'string') merged.set(project.id, project)
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
activeProjectId: typeof backupConfig.activeProjectId === 'string' ? backupConfig.activeProjectId : (localConfig?.activeProjectId ?? null),
|
|
513
|
-
globalSkills: Array.isArray(backupConfig.globalSkills) ? backupConfig.globalSkills : (Array.isArray(localConfig?.globalSkills) ? localConfig.globalSkills : []),
|
|
514
|
-
projects: [...merged.values()],
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
530
|
async function restoreValidatedBackup(backup, mode = 'replace') {
|
|
519
531
|
const merge = mode === 'merge'
|
|
520
532
|
const { sections } = backup
|
|
@@ -544,16 +556,10 @@ async function restoreValidatedBackup(backup, mode = 'replace') {
|
|
|
544
556
|
summary.customProviders = countKeys(value)
|
|
545
557
|
}
|
|
546
558
|
|
|
547
|
-
if (sections.projects !== undefined) {
|
|
548
|
-
const value = merge ? mergeProjectConfig(await readProjectConfigData(), sections.projects) : sections.projects
|
|
549
|
-
await writeProjectConfigData(value)
|
|
550
|
-
await initializeActiveProject()
|
|
551
|
-
setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
|
|
552
|
-
summary.projects = value.projects.length
|
|
553
|
-
}
|
|
554
|
-
|
|
555
559
|
if (sections.scheduledTasks !== undefined) {
|
|
556
|
-
const
|
|
560
|
+
const projectIds = await localProjectIds()
|
|
561
|
+
const safeTasks = pauseScheduledTasksWithMissingProjects(sections.scheduledTasks, projectIds)
|
|
562
|
+
const value = merge ? mergeRecordStore(await readStore('scheduled-tasks'), safeTasks) : safeTasks
|
|
557
563
|
await writeStore('scheduled-tasks', value)
|
|
558
564
|
summary.scheduledTasks = countKeys(value)
|
|
559
565
|
}
|
|
@@ -587,7 +593,7 @@ export async function handleBackupApi(req, res, url) {
|
|
|
587
593
|
if (req.method === 'POST' && url.pathname === '/api/backup/inspect') {
|
|
588
594
|
await ensureStorage()
|
|
589
595
|
const body = await readJsonBody(req)
|
|
590
|
-
sendJson(res, 200, inspectBackup(body))
|
|
596
|
+
sendJson(res, 200, await inspectBackup(body))
|
|
591
597
|
return
|
|
592
598
|
}
|
|
593
599
|
|
|
@@ -595,10 +601,11 @@ export async function handleBackupApi(req, res, url) {
|
|
|
595
601
|
await ensureStorage()
|
|
596
602
|
const text = await readTextBody(req)
|
|
597
603
|
const { backup, ignoredConversations } = extractSettingsBackupFromText(text)
|
|
598
|
-
const { backup: validBackup, invalidSections } = validateSettingsImportBackup(backup)
|
|
599
|
-
const inspect = inspectBackup(validBackup)
|
|
604
|
+
const { backup: validBackup, invalidSections, ignoredProjects } = validateSettingsImportBackup(backup)
|
|
605
|
+
const inspect = await inspectBackup(validBackup)
|
|
606
|
+
if (ignoredProjects) inspect.warnings.push('Project lists and project-specific settings are local machine data and will not be imported.')
|
|
600
607
|
const token = await writePendingImportBackup(validBackup)
|
|
601
|
-
sendJson(res, 200, { ...inspect, invalidSections, ignoredConversations, importToken: token })
|
|
608
|
+
sendJson(res, 200, { ...inspect, invalidSections, ignoredConversations, ignoredProjects, importToken: token })
|
|
602
609
|
return
|
|
603
610
|
}
|
|
604
611
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
2
|
import { readStore, writeStore, atomicUpdate, getComparable, getStoreRevision, readSessionStoreScoped, readSessionValue, writeSessionValue, deleteSessionValue, ensureStorage, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
|
|
3
|
+
import { AUTO_ARCHIVE_SETTINGS_KEY, archiveInactiveSessions, normalizeAutoArchiveSettings } from '../auto-archive.mjs'
|
|
3
4
|
import { directorySize } from '../utils/workspace.mjs'
|
|
4
5
|
|
|
5
6
|
const metadataIndexCache = new Map()
|
|
@@ -194,6 +195,9 @@ export async function handleStorageApi(req, res, url) {
|
|
|
194
195
|
data[key] = body?.value
|
|
195
196
|
return data
|
|
196
197
|
})
|
|
198
|
+
if (store === 'settings' && key === AUTO_ARCHIVE_SETTINGS_KEY && normalizeAutoArchiveSettings(body?.value).enabled) {
|
|
199
|
+
await archiveInactiveSessions()
|
|
200
|
+
}
|
|
197
201
|
sendJson(res, 200, { ok: true })
|
|
198
202
|
return
|
|
199
203
|
}
|
|
@@ -662,53 +662,105 @@ async function handleWorkspaceFile(req, res, url) {
|
|
|
662
662
|
})
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
665
|
+
function createWorkspacePreviewError(message, statusCode, previewCode) {
|
|
666
|
+
const error = new Error(message)
|
|
667
|
+
error.statusCode = statusCode
|
|
668
|
+
error.previewCode = previewCode
|
|
669
|
+
return error
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function workspacePreviewIssueFromError(error, requestedPath = '') {
|
|
673
|
+
let status = error?.statusCode || 500
|
|
674
|
+
let code = error?.previewCode || 'PREVIEW_SERVICE_FAILED'
|
|
675
|
+
|
|
676
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
677
|
+
status = 404
|
|
678
|
+
code = 'PREVIEW_FILE_NOT_FOUND'
|
|
679
|
+
} else if (error?.name === 'URIError') {
|
|
680
|
+
status = 400
|
|
681
|
+
code = 'PREVIEW_INVALID_PATH'
|
|
682
|
+
} else if (error?.code === 'EACCES' || error?.code === 'EPERM') {
|
|
683
|
+
status = 403
|
|
684
|
+
code = 'PREVIEW_PERMISSION_DENIED'
|
|
685
|
+
} else if (status === 403 && !error?.previewCode) {
|
|
686
|
+
code = 'PREVIEW_PERMISSION_DENIED'
|
|
687
|
+
} else if (status === 400 && !error?.previewCode) {
|
|
688
|
+
code = 'PREVIEW_INVALID_PATH'
|
|
673
689
|
}
|
|
674
690
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
691
|
+
return {
|
|
692
|
+
status,
|
|
693
|
+
payload: {
|
|
694
|
+
error: error?.message || 'Internal server error',
|
|
695
|
+
code,
|
|
696
|
+
path: requestedPath,
|
|
697
|
+
},
|
|
681
698
|
}
|
|
699
|
+
}
|
|
682
700
|
|
|
683
|
-
|
|
701
|
+
export async function inspectWorkspacePreviewFile(context, relativePath) {
|
|
684
702
|
const file = resolveWorkspacePath(relativePath, context)
|
|
685
703
|
await assertSafeWorkspacePath(file, context)
|
|
686
704
|
const extension = path.extname(file).toLowerCase()
|
|
687
705
|
if (!PREVIEW_ALLOWED_EXTENSIONS.has(extension)) {
|
|
688
|
-
|
|
689
|
-
error.statusCode = 415
|
|
690
|
-
throw error
|
|
706
|
+
throw createWorkspacePreviewError('Unsupported preview file type', 415, 'PREVIEW_UNSUPPORTED_TYPE')
|
|
691
707
|
}
|
|
708
|
+
|
|
692
709
|
const stat = await fs.stat(file)
|
|
693
710
|
if (!stat.isFile()) {
|
|
694
|
-
|
|
695
|
-
error.statusCode = 400
|
|
696
|
-
throw error
|
|
711
|
+
throw createWorkspacePreviewError('Path is not a file', 400, 'PREVIEW_INVALID_PATH')
|
|
697
712
|
}
|
|
698
713
|
if (stat.size > MAX_STATIC_PREVIEW_BYTES) {
|
|
699
|
-
|
|
700
|
-
error.statusCode = 413
|
|
701
|
-
throw error
|
|
714
|
+
throw createWorkspacePreviewError('File is too large to preview', 413, 'PREVIEW_FILE_TOO_LARGE')
|
|
702
715
|
}
|
|
703
716
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
717
|
+
return {
|
|
718
|
+
file,
|
|
719
|
+
stat,
|
|
720
|
+
contentType: previewContentType(file),
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function handleWorkspacePreview(req, res, url) {
|
|
725
|
+
let relativePath = ''
|
|
726
|
+
try {
|
|
727
|
+
const prefix = '/api/workspace/preview/'
|
|
728
|
+
const tail = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : ''
|
|
729
|
+
const slashIndex = tail.indexOf('/')
|
|
730
|
+
if (slashIndex <= 0) {
|
|
731
|
+
throw createWorkspacePreviewError('projectId and path are required', 400, 'PREVIEW_INVALID_PATH')
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const projectId = decodeURIComponent(tail.slice(0, slashIndex))
|
|
735
|
+
relativePath = decodeURIComponent(tail.slice(slashIndex + 1))
|
|
736
|
+
if (!projectId || !relativePath) {
|
|
737
|
+
throw createWorkspacePreviewError('projectId and path are required', 400, 'PREVIEW_INVALID_PATH')
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const context = await projectContextFromId(projectId)
|
|
741
|
+
const preview = await inspectWorkspacePreviewFile(context, relativePath)
|
|
742
|
+
if (url.searchParams.get('__quickforge_check') === '1') {
|
|
743
|
+
sendJson(res, 200, {
|
|
744
|
+
ok: true,
|
|
745
|
+
path: relativePath,
|
|
746
|
+
size: preview.stat.size,
|
|
747
|
+
contentType: preview.contentType,
|
|
748
|
+
})
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
res.writeHead(200, {
|
|
753
|
+
'content-type': preview.contentType,
|
|
754
|
+
'cache-control': 'no-store',
|
|
755
|
+
'x-content-type-options': 'nosniff',
|
|
756
|
+
})
|
|
757
|
+
const buffer = await fs.readFile(preview.file)
|
|
758
|
+
res.end(buffer)
|
|
759
|
+
} catch (error) {
|
|
760
|
+
const issue = workspacePreviewIssueFromError(error, relativePath)
|
|
761
|
+
if (issue.status >= 500) logger.error('Workspace preview failed', { error: issue.payload.error, path: relativePath })
|
|
762
|
+
sendJson(res, issue.status, issue.payload)
|
|
763
|
+
}
|
|
712
764
|
}
|
|
713
765
|
|
|
714
766
|
async function handleWorkspaceResolvePath(req, res) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
let persistenceQueue = Promise.resolve()
|
|
2
|
+
|
|
3
|
+
export function withSessionPersistenceLock(operation) {
|
|
4
|
+
const result = persistenceQueue
|
|
5
|
+
.catch(() => undefined)
|
|
6
|
+
.then(operation)
|
|
7
|
+
persistenceQueue = result.then(() => undefined, () => undefined)
|
|
8
|
+
return result
|
|
9
|
+
}
|
package/server/storage.mjs
CHANGED
|
@@ -604,6 +604,17 @@ export async function writeSessionValue(sessionId, value) {
|
|
|
604
604
|
})
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
export async function atomicSessionValueUpdate(sessionId, updateFn) {
|
|
608
|
+
return enqueueWrite('sessions', async () => {
|
|
609
|
+
await ensureStorage()
|
|
610
|
+
const current = await readSessionValue(sessionId)
|
|
611
|
+
if (!current) return null
|
|
612
|
+
const updated = updateFn(current)
|
|
613
|
+
await writeSessionValueFile(sessionId, updated)
|
|
614
|
+
return updated
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
|
|
607
618
|
export async function deleteSessionValue(sessionId) {
|
|
608
619
|
return enqueueWrite('sessions', async () => {
|
|
609
620
|
const bucket = await findSessionBucket(sessionId)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{$ as t,Ot as n,St as r,T as i,Tt as a,a as ee,c as o}from"./icons-DAxUA0e-.js";import{i as s,n as c}from"./react-vendor-CZsiwuxm.js";import{$ as te,Q as l,V as ne,Z as re,et as ie,lt as u,mt as d,st as f,tt as ae}from"./index-DGQbvw7v.js";var p=e(n(),1),oe=s(),m=c(),h=144,g=82,_=4,v=8;function y(e){return JSON.stringify({provider:e.provider,modelId:e.id,api:e.api,baseUrl:e.baseUrl})}function b(e){if(!e)return{mode:`inherit`};try{let t=JSON.parse(e);return{mode:`fixed`,provider:String(t.provider||``),modelId:String(t.modelId||``),api:t.api?String(t.api):void 0,baseUrl:t.baseUrl?String(t.baseUrl):void 0}}catch{return{mode:`inherit`}}}function x(e){return!e||e.mode!==`fixed`?``:JSON.stringify({provider:e.provider,modelId:e.modelId,api:e.api,baseUrl:e.baseUrl})}function se(e){return e.name||`${e.provider}/${e.id}`}function S(){return{name:``,label:``,description:``,systemPrompt:``,allowedTools:[`read_file`,`grep_files`],maxRuntimeMs:`1800000`,maxToolCalls:`300`,enabledAsSubagent:!0,modelMode:`inherit`,fixedModelValue:``,thinkingLevel:`inherit`}}function ce(e){return{name:e.name,label:e.label,description:e.description??``,systemPrompt:e.systemPrompt??``,allowedTools:e.allowedTools??[],maxRuntimeMs:String(e.maxRuntimeMs??18e5),maxToolCalls:String(e.maxToolCalls??300),enabledAsSubagent:e.enabledAsSubagent,modelMode:e.model?.mode===`fixed`?`fixed`:`inherit`,fixedModelValue:x(e.model),thinkingLevel:e.thinkingLevel??`inherit`}}function le(e){return{name:e.name.trim().toLowerCase(),label:e.label.trim(),description:e.description.trim(),systemPrompt:e.systemPrompt.trim(),allowedTools:e.allowedTools,maxRuntimeMs:Number(e.maxRuntimeMs||18e5),maxToolCalls:Number(e.maxToolCalls||300),enabledAsSubagent:e.enabledAsSubagent,model:e.modelMode===`fixed`?b(e.fixedModelValue):{mode:`inherit`},thinkingLevel:e.thinkingLevel}}function C(e){return!!(e.name.trim()&&e.label.trim()&&e.allowedTools.length>0)}async function w(e,t){let n=await fetch(e,{...t,headers:{"content-type":`application/json`,...t?.headers}}),r=await n.json().catch(()=>null);if(!n.ok)throw Error(r?.error||`请求失败`);return r}function T(){let[e,n]=(0,p.useState)([]),[s,c]=(0,p.useState)([]),[x,T]=(0,p.useState)(!1),[E,D]=(0,p.useState)(null),[O,k]=(0,p.useState)(()=>S()),[A,j]=(0,p.useState)(!1),[M,N]=(0,p.useState)(``),[P,F]=(0,p.useState)(!1),[I,ue]=(0,p.useState)(),[L,de]=(0,p.useState)([]),[fe,pe]=(0,p.useState)(`off`),[R,z]=(0,p.useState)(``),[B,V]=(0,p.useState)(null),[H,U]=(0,p.useState)(null);async function W(){let[e,t]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);n(e.agents),c(t.tools)}(0,p.useEffect)(()=>{let e=!1;async function t(){try{let[t,r]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);if(e)return;n(t.agents),c(r.tools)}catch(t){e||z(t instanceof Error?t.message:d(`requestFailed`))}}return t(),()=>{e=!0}},[]),(0,p.useEffect)(()=>{let e=!1;async function t(){try{let t=await te(),n=await l(t);de(n);let r=await ie(t),i=r.model??await ae(t)??n[0];if(e)return;ue(i),pe(r.thinkingLevel??re(i))}catch{}}return t(),()=>{e=!0}},[]),(0,p.useEffect)(()=>{if(!B)return;let e=()=>{V(null),U(null)},t=t=>{t.key===`Escape`&&e()};return window.addEventListener(`click`,e),window.addEventListener(`blur`,e),window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),document.addEventListener(`keydown`,t),()=>{window.removeEventListener(`click`,e),window.removeEventListener(`blur`,e),window.removeEventListener(`resize`,e),window.removeEventListener(`scroll`,e,!0),document.removeEventListener(`keydown`,t)}},[B]);let G=(0,p.useMemo)(()=>e.find(e=>e.id===E)??null,[e,E]),K=(0,p.useMemo)(()=>e.find(e=>e.id===B)??null,[e,B]),q=!!G?.readonly,J=!!(G?.readonly&&!G?.builtin),Y=(0,p.useMemo)(()=>L.find(e=>y(e)===O.fixedModelValue),[O.fixedModelValue,L]),X=O.modelMode===`fixed`&&!!Y&&Y?.reasoning!==!0;function me(e,t){if(e.stopPropagation(),B===t){V(null),U(null);return}let n=e.currentTarget.getBoundingClientRect(),r=Math.max(v,Math.min(n.right-h,window.innerWidth-h-v)),i=n.bottom+_,a=n.top-_-g;U({left:r,top:i+g<=window.innerHeight-v?i:Math.max(v,a)}),V(t)}function Z(e,t){k(n=>({...n,[e]:t}))}function he(e){k(t=>({...t,allowedTools:t.allowedTools.includes(e)?t.allowedTools.filter(t=>t!==e):[...t.allowedTools,e]}))}function ge(){D(null),k(S()),N(``),z(``),T(!0)}function Q(e){D(e.id),k(ce(e)),N(``),z(``),T(!0)}function $(){A||P||(T(!1),D(null),k(S()),N(``))}async function _e(){let e=M.trim();if(!e){z(d(`aiFillAgentInputRequired`));return}if(!I){z(d(`aiFillAgentNoModel`));return}F(!0),z(``);try{let t=await w(`/api/agent-profiles/ai-fill`,{method:`POST`,body:JSON.stringify({instruction:e,model:I,thinkingLevel:fe})});k(e=>({...e,name:t.agent.name,label:t.agent.label,description:t.agent.description,systemPrompt:t.agent.systemPrompt}))}catch(e){z(e instanceof Error?e.message:d(`aiFillAgentFailed`))}finally{F(!1)}}async function ve(){if(C(O)){j(!0),z(``);try{let e=G?.builtin?{model:O.modelMode===`fixed`?b(O.fixedModelValue):{mode:`inherit`}}:le({...O,thinkingLevel:X?`off`:O.thinkingLevel});E?await w(`/api/agent-profiles/${encodeURIComponent(E)}`,{method:`PATCH`,body:JSON.stringify(e)}):await w(`/api/agent-profiles`,{method:`POST`,body:JSON.stringify(e)}),$(),await W()}catch(e){z(e instanceof Error?e.message:d(`requestFailed`))}finally{j(!1)}}}async function ye(e){if(e.builtin||e.readonly)return;let t=!e.enabledAsSubagent,r=e.enabledAsSubagent;n(n=>n.map(n=>n.id===e.id?{...n,enabledAsSubagent:t}:n)),V(null);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`PATCH`,body:JSON.stringify({enabledAsSubagent:t})})}catch(t){n(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:r}:t)),z(t instanceof Error?t.message:d(`requestFailed`))}}async function be(e){if(!(e.builtin||e.readonly)&&await ne({description:d(`confirmDeleteAgent`),confirmLabel:d(`confirmDelete`),cancelLabel:d(`cancel`),variant:`destructive`})){z(``);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await W()}catch(e){z(e instanceof Error?e.message:d(`requestFailed`))}}}return x?(0,m.jsxs)(`div`,{className:`quickforge-settings-stack`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-heading`,children:(0,m.jsxs)(`h3`,{className:`quickforge-settings-title`,children:[G?.builtin?d(`builtinAgentModelSettings`):d(G?`editAgent`:`createAgent`),(0,m.jsx)(f,{label:G?.builtin?d(`builtinAgentModelOnly`):G?.readonly?d(`readonlyAgentDescription`):d(`agentsDescription`)})]})}),(0,m.jsxs)(`section`,{className:`quickforge-settings-section`,"aria-label":d(G?`editAgent`:`createAgent`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`button`,{className:`quickforge-settings-button quickforge-settings-button-secondary`,type:`button`,onClick:$,disabled:A||P,children:[(0,m.jsx)(a,{className:`mr-2 size-4`}),d(`back`)]}),(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-row-title`,children:G?.builtin?d(`builtinAgentModelSettings`):d(G?`editAgent`:`createAgent`)}),G?.builtin?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:d(`builtinAgentModelOnly`)}):G?.readonly?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:d(`readonlyAgentDescription`)}):null]})]}),(0,m.jsx)(`div`,{className:`px-5 py-4`,children:(0,m.jsxs)(`div`,{className:`space-y-4`,children:[(0,m.jsxs)(`div`,{className:`rounded-2xl border border-border bg-muted/20 p-3`,children:[(0,m.jsxs)(`div`,{className:`mb-2 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,m.jsx)(o,{className:`size-4 text-primary`}),d(`aiFillAgent`),(0,m.jsx)(f,{label:d(`aiFillAgentDescription`)})]}),(0,m.jsx)(`textarea`,{className:`min-h-20 w-full resize-y rounded-xl border border-input bg-background px-3 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground/65 focus:border-ring disabled:opacity-60`,value:M,disabled:q||P,onChange:e=>N(e.target.value),placeholder:d(`aiFillAgentPlaceholder`)}),(0,m.jsx)(`div`,{className:`mt-2 flex justify-end`,children:(0,m.jsxs)(u,{variant:`outline`,size:`sm`,onClick:()=>void _e(),disabled:q||P||!M.trim(),children:[(0,m.jsx)(o,{className:`mr-1 size-3.5`}),d(P?`aiFillAgentLoading`:`aiFillAgent`)]})})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentName`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.name,disabled:q,onChange:e=>Z(`name`,e.target.value),placeholder:`reviewer`})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentLabel`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.label,disabled:q,onChange:e=>Z(`label`,e.target.value),placeholder:d(`agentLabelPlaceholder`)})]})]}),G?(0,m.jsxs)(`div`,{className:`rounded-xl border border-border bg-muted/20 px-3 py-2 text-sm`,children:[(0,m.jsx)(`div`,{className:`text-xs font-medium text-muted-foreground`,children:d(`agentSourcePath`)}),(0,m.jsx)(`div`,{className:`mt-1 truncate font-mono text-xs text-foreground`,title:G.source?`${G.source}${G.relativePath?` · ${G.relativePath}`:``}`:void 0,children:G.source?`${G.source}${G.relativePath?` · ${G.relativePath}`:``}`:G.builtin?d(`builtinAgent`):`-`})]}):null,(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentDescription`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.description,disabled:q,onChange:e=>Z(`description`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentSystemPrompt`),(0,m.jsx)(`textarea`,{className:`mt-1 min-h-36 w-full resize-y rounded-xl border border-input bg-background px-3 py-2 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.systemPrompt,disabled:q,onChange:e=>Z(`systemPrompt`,e.target.value)})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentModelMode`),(0,m.jsxs)(`select`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.modelMode,disabled:J,onChange:e=>Z(`modelMode`,e.target.value),children:[(0,m.jsx)(`option`,{value:`inherit`,children:d(`agentModelInherit`)}),(0,m.jsx)(`option`,{value:`fixed`,children:d(`agentModelFixed`)})]})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentFixedModel`),(0,m.jsxs)(`select`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.fixedModelValue,disabled:J||O.modelMode!==`fixed`,onChange:e=>Z(`fixedModelValue`,e.target.value),children:[(0,m.jsx)(`option`,{value:``,children:d(`agentModelInherit`)}),L.map(e=>(0,m.jsx)(`option`,{value:y(e),children:se(e)},y(e)))]})]})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentThinkingLevel`),(0,m.jsxs)(`select`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:X&&O.thinkingLevel!==`inherit`?`off`:O.thinkingLevel,disabled:q||X,onChange:e=>Z(`thinkingLevel`,e.target.value),children:[(0,m.jsx)(`option`,{value:`inherit`,children:d(`agentThinkingInherit`)}),(0,m.jsx)(`option`,{value:`off`,children:d(`thinkingOff`)}),(0,m.jsx)(`option`,{value:`low`,children:d(`thinkingLow`)}),(0,m.jsx)(`option`,{value:`medium`,children:d(`thinkingMedium`)}),(0,m.jsx)(`option`,{value:`high`,children:d(`thinkingHigh`)}),(0,m.jsx)(`option`,{value:`xhigh`,children:d(`thinkingXHigh`)})]}),(0,m.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:d(X?`agentThinkingUnsupported`:`agentThinkingDescription`)})]}),(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`div`,{className:`mb-2 text-sm font-medium text-foreground`,children:d(`allowedTools`)}),(0,m.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2`,children:s.map(e=>(0,m.jsxs)(`label`,{className:`flex items-start gap-2 rounded-xl border border-border bg-muted/20 p-3 text-sm disabled:opacity-60`,children:[(0,m.jsx)(`input`,{type:`checkbox`,className:`mt-1`,disabled:q,checked:O.allowedTools.includes(e.name),onChange:()=>he(e.name)}),(0,m.jsxs)(`span`,{children:[(0,m.jsx)(`span`,{className:`font-medium text-foreground`,children:e.label}),(0,m.jsx)(`span`,{className:`ml-2 font-mono text-xs text-muted-foreground`,children:e.name}),e.riskLevel===`dangerous`?(0,m.jsx)(`span`,{className:`ml-2 rounded-full bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700`,children:d(`highRiskTool`)}):null,(0,m.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:e.description})]})]},e.name))})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`maxRuntimeMs`),(0,m.jsx)(`input`,{type:`number`,className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.maxRuntimeMs,disabled:q,onChange:e=>Z(`maxRuntimeMs`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`maxToolCalls`),(0,m.jsx)(`input`,{type:`number`,className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.maxToolCalls,disabled:q,onChange:e=>Z(`maxToolCalls`,e.target.value)})]})]}),(0,m.jsxs)(`label`,{className:`flex items-center gap-2 text-sm text-foreground`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:O.enabledAsSubagent,disabled:q,onChange:e=>Z(`enabledAsSubagent`,e.target.checked)}),d(`enabledAsSubagent`)]}),R?(0,m.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:R}):null]})}),(0,m.jsxs)(`div`,{className:`quickforge-settings-divider flex justify-end gap-2 px-5 py-4`,children:[(0,m.jsx)(u,{variant:`outline`,onClick:$,disabled:A||P,children:d(`cancel`)}),(0,m.jsx)(u,{onClick:ve,disabled:A||P||J||!G?.builtin&&!C(O)||O.modelMode===`fixed`&&!O.fixedModelValue,children:d(`save`)})]})]})]}):(0,m.jsxs)(`div`,{className:`quickforge-settings-stack`,children:[(0,m.jsxs)(`section`,{className:`quickforge-settings-section`,"aria-label":d(`agentsTab`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-title`,children:[(0,m.jsx)(r,{className:`size-4 text-primary`}),d(`agentsTab`)]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:d(`agentsDescription`)})]}),(0,m.jsx)(`button`,{className:`quickforge-settings-button quickforge-settings-button-primary`,type:`button`,onClick:ge,children:d(`createAgent`)})]}),R?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:R}):null,e.length===0?(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`loading`)}):e.map(e=>(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item quickforge-agent-profile-row`,role:`button`,tabIndex:0,onClick:()=>Q(e),onKeyDown:t=>{t.target===t.currentTarget&&(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),Q(e))},children:[(0,m.jsx)(`div`,{className:`quickforge-settings-list-item-main quickforge-agent-profile-row-main`,children:(0,m.jsxs)(`div`,{className:`quickforge-agent-profile-summary`,children:[(0,m.jsx)(`span`,{className:`quickforge-agent-profile-label`,title:e.label,children:e.label}),e.description?(0,m.jsx)(`span`,{className:`quickforge-agent-profile-description`,title:e.description,children:e.description}):null]})}),(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":e.builtin||e.readonly?`true`:`false`,title:e.enabledAsSubagent?d(`disableAsSubagent`):d(`enableAsSubagent`),children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:e.enabledAsSubagent,disabled:e.builtin||e.readonly,onChange:()=>void ye(e)}),(0,m.jsx)(`span`,{"aria-hidden":`true`})]}),(0,m.jsx)(`button`,{className:`quickforge-settings-icon-action`,type:`button`,onClick:t=>me(t,e.id),title:d(`moreActions`),"aria-label":d(`moreActions`),"aria-haspopup":`menu`,"aria-expanded":B===e.id,children:(0,m.jsx)(t,{className:`size-4`})})]})]},e.id))]}),K&&H?(0,oe.createPortal)((0,m.jsxs)(`div`,{className:`fixed z-50 w-36 overflow-hidden rounded-xl border border-border bg-popover py-1 text-sm shadow-quickforge`,style:{left:H.left,top:H.top},role:`menu`,"aria-label":d(`moreActions`),onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`button`,{className:`flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50`,type:`button`,role:`menuitem`,disabled:K.readonly&&!K.builtin,onClick:()=>{V(null),U(null),Q(K)},children:[(0,m.jsx)(i,{className:`size-3.5`}),K.builtin?d(`builtinAgentModelSettings`):d(`editTask`)]}),(0,m.jsxs)(`button`,{className:`flex w-full items-center gap-2 px-3 py-2 text-left text-destructive hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50`,type:`button`,role:`menuitem`,disabled:K.builtin||K.readonly,onClick:()=>{V(null),U(null),be(K)},children:[(0,m.jsx)(ee,{className:`size-3.5`}),d(`delete`)]})]}),document.body):null]})}export{T as AgentProfilesPage};
|