@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.
Files changed (34) hide show
  1. package/dist/assets/AgentProfilesPage-__uY0AvK.js +1 -0
  2. package/dist/assets/{ChatPanelHost-CxEotTsy.js → ChatPanelHost-DIs_vWFX.js} +1 -1
  3. package/dist/assets/{PluginsPage-DZlqGFLn.js → PluginsPage-CKyWhlCo.js} +1 -1
  4. package/dist/assets/ScheduledTasksPage-KtJoeJYt.js +2 -0
  5. package/dist/assets/{SettingsWorkspacePage-BJq11OPD.js → SettingsWorkspacePage-DwqEnUmX.js} +362 -349
  6. package/dist/assets/{SharedConversationPage-BCR8xzii.js → SharedConversationPage-yujCRsbe.js} +1 -1
  7. package/dist/assets/TerminalDock-B9xKnimU.js +2 -0
  8. package/dist/assets/WorkspaceInspector-CE6RD6Ys.js +13 -0
  9. package/dist/assets/icons-pPRMD2tE.js +1 -0
  10. package/dist/assets/index-BSXpUDCq.js +63 -0
  11. package/dist/assets/index-DpO7jEGP.css +3 -0
  12. package/dist/assets/{mcp-servers-dialog-BHljnwYv.js → mcp-servers-dialog-CaQTvGiW.js} +2 -2
  13. package/dist/assets/{monaco-DEQoYzYI.js → monaco-CPwJMUsl.js} +1 -1
  14. package/dist/assets/{react-vendor-CZsiwuxm.js → react-vendor-CLbWF1Oy.js} +1 -1
  15. package/dist/assets/{skills-dialog-8rn1XU9U.js → skills-dialog-D18mxNyW.js} +1 -1
  16. package/dist/index.html +6 -6
  17. package/package.json +1 -1
  18. package/server/agent-manager.mjs +19 -14
  19. package/server/auto-archive.mjs +137 -0
  20. package/server/auto-compaction.mjs +15 -18
  21. package/server/conversation-compaction.mjs +69 -3
  22. package/server/index.mjs +6 -1
  23. package/server/routes/backup.mjs +79 -72
  24. package/server/routes/storage.mjs +4 -0
  25. package/server/routes/workspace.mjs +84 -32
  26. package/server/session-persistence-lock.mjs +9 -0
  27. package/server/storage.mjs +11 -0
  28. package/dist/assets/AgentProfilesPage-Be0oZDW5.js +0 -1
  29. package/dist/assets/ScheduledTasksPage-DTkj-0O4.js +0 -2
  30. package/dist/assets/TerminalDock-Bs5b-YNp.js +0 -2
  31. package/dist/assets/WorkspaceInspector-BP0XyAlE.js +0 -13
  32. package/dist/assets/icons-DAxUA0e-.js +0 -1
  33. package/dist/assets/index-BZC61KZg.css +0 -3
  34. 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.slice(),
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: sourceMessages.slice(0, tailStart),
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()
@@ -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', 'projects', 'scheduledTasks']
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 data = backup.data && typeof backup.data === 'object' && !Array.isArray(backup.data)
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 === 'projects'
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 value = merge ? mergeRecordStore(await readStore('scheduled-tasks'), sections.scheduledTasks) : sections.scheduledTasks
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
- async function handleWorkspacePreview(req, res, url) {
666
- const prefix = '/api/workspace/preview/'
667
- const tail = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : ''
668
- const slashIndex = tail.indexOf('/')
669
- if (slashIndex <= 0) {
670
- const error = new Error('projectId and path are required')
671
- error.statusCode = 400
672
- throw error
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
- const projectId = decodeURIComponent(tail.slice(0, slashIndex))
676
- const relativePath = decodeURIComponent(tail.slice(slashIndex + 1))
677
- if (!projectId || !relativePath) {
678
- const error = new Error('projectId and path are required')
679
- error.statusCode = 400
680
- throw error
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
- const context = await projectContextFromId(projectId)
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
- const error = new Error('Unsupported preview file type')
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
- const error = new Error('Path is not a file')
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
- const error = new Error('File is too large to preview')
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
- const contentType = previewContentType(file)
705
- res.writeHead(200, {
706
- 'content-type': contentType,
707
- 'cache-control': 'no-store',
708
- 'x-content-type-options': 'nosniff',
709
- })
710
- const buffer = await fs.readFile(file)
711
- res.end(buffer)
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
+ }
@@ -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};