@shawnstack/quickforge 1.7.1 → 1.7.2

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 (35) hide show
  1. package/dist/assets/AgentProfilesPage-BFgyFa5e.js +1 -0
  2. package/dist/assets/ChatPanelHost-Cld9HcVz.js +288 -0
  3. package/dist/assets/{PluginsPage-CKyWhlCo.js → PluginsPage-DEMqwhOA.js} +1 -1
  4. package/dist/assets/ScheduledTasksPage-DTP_gedp.js +2 -0
  5. package/dist/assets/{SettingsWorkspacePage-DwqEnUmX.js → SettingsWorkspacePage-Dr9zT5Bs.js} +112 -112
  6. package/dist/assets/SharedConversationPage-op3DRwcw.js +1 -0
  7. package/dist/assets/{TerminalDock-B9xKnimU.js → TerminalDock-BL_UovwU.js} +2 -2
  8. package/dist/assets/WorkspaceInspector-73dhAime.js +13 -0
  9. package/dist/assets/icons-BP8YOS-Z.js +1 -0
  10. package/dist/assets/index-Cu2bBHLv.css +3 -0
  11. package/dist/assets/index-yV1wtqTr.js +66 -0
  12. package/dist/assets/mcp-servers-dialog-Bp6kbIup.js +5 -0
  13. package/dist/assets/{monaco-CPwJMUsl.js → monaco-BTsVCDWS.js} +1 -1
  14. package/dist/assets/{react-vendor-CLbWF1Oy.js → react-vendor-Dr5xvL-e.js} +1 -1
  15. package/dist/assets/{skills-dialog-D18mxNyW.js → skills-dialog-DiJAUfvW.js} +1 -1
  16. package/dist/index.html +6 -6
  17. package/package.json +1 -1
  18. package/server/acp/server.mjs +19 -7
  19. package/server/mcp/registry.mjs +54 -14
  20. package/server/plugins/loader.mjs +9 -1
  21. package/server/plugins/registry.mjs +32 -10
  22. package/server/routes/mcp.mjs +0 -5
  23. package/server/routes/scheduled-tasks.mjs +72 -32
  24. package/server/routes/workspace.mjs +87 -17
  25. package/server/utils/scheduled-tasks.mjs +23 -10
  26. package/server/utils/workspace.mjs +25 -3
  27. package/dist/assets/AgentProfilesPage-__uY0AvK.js +0 -1
  28. package/dist/assets/ChatPanelHost-DIs_vWFX.js +0 -291
  29. package/dist/assets/ScheduledTasksPage-KtJoeJYt.js +0 -2
  30. package/dist/assets/SharedConversationPage-yujCRsbe.js +0 -1
  31. package/dist/assets/WorkspaceInspector-CE6RD6Ys.js +0 -13
  32. package/dist/assets/icons-pPRMD2tE.js +0 -1
  33. package/dist/assets/index-BSXpUDCq.js +0 -63
  34. package/dist/assets/index-DpO7jEGP.css +0 -3
  35. package/dist/assets/mcp-servers-dialog-CaQTvGiW.js +0 -5
@@ -1,7 +1,7 @@
1
1
  import { streamSimple } from '@earendil-works/pi-ai/compat'
2
2
  import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
3
3
  import { readStore, atomicUpdate } from '../storage.mjs'
4
- import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'
4
+ import { createAgent, getSessionEventBus, agentEvents, persistSessionState, abortRun } from '../agent-manager.mjs'
5
5
  import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
6
6
  import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
7
7
  import { logger } from '../utils/logger.mjs'
@@ -436,21 +436,30 @@ async function updateTask(taskId, updater) {
436
436
  return updated
437
437
  }
438
438
 
439
+ function recurringTaskStatusRepair(task, now) {
440
+ if (task?.status !== 'completed' || !isRecurringTask(task)) return null
441
+ const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > now.getTime()
442
+ ? task.nextRunAt
443
+ : calculateNextRun(task, now)
444
+ if (!nextRunAt) return null
445
+ return {
446
+ ...task,
447
+ status: 'enabled',
448
+ nextRunAt,
449
+ scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
450
+ updatedAt: now.toISOString(),
451
+ }
452
+ }
453
+
439
454
  async function repairRecurringTaskStatuses() {
455
+ const now = new Date()
456
+ const snapshot = await readStore(STORE)
457
+ if (!Object.values(snapshot).some((task) => recurringTaskStatusRepair(task, now))) return
458
+
440
459
  await atomicUpdate(STORE, (data) => {
441
460
  for (const [taskId, task] of Object.entries(data)) {
442
- if (task?.status !== 'completed' || !isRecurringTask(task)) continue
443
- const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > Date.now()
444
- ? task.nextRunAt
445
- : calculateNextRun(task)
446
- if (!nextRunAt) continue
447
- data[taskId] = {
448
- ...task,
449
- status: 'enabled',
450
- nextRunAt,
451
- scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
452
- updatedAt: new Date().toISOString(),
453
- }
461
+ const repaired = recurringTaskStatusRepair(task, now)
462
+ if (repaired) data[taskId] = repaired
454
463
  }
455
464
  return data
456
465
  })
@@ -566,14 +575,15 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
566
575
  onStarted?.({ taskId: task.id, runId, sessionId })
567
576
 
568
577
  const eventBus = getSessionEventBus(sessionId)
578
+ const runtimeLimitMs = Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000))
579
+ let timeout = null
580
+ let handler = null
581
+ let timedOut = false
582
+ let resolveFinished
569
583
  const finished = new Promise((resolve) => {
570
- const cleanup = (handler, timeout) => {
571
- clearTimeout(timeout)
572
- eventBus?.removeListener('agent_event', handler)
573
- }
574
- const handler = (event) => {
584
+ resolveFinished = resolve
585
+ handler = (event) => {
575
586
  if (event.type !== 'agent_end') return
576
- cleanup(handler, timeout)
577
587
  const errorMessage = event.errorMessage || session.agent.state.errorMessage
578
588
  const aborted = session.status === 'aborted' || session.agent.state.messages.some((message) => message?.role === 'assistant' && message?.stopReason === 'aborted')
579
589
  resolve({
@@ -583,24 +593,54 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
583
593
  messages: event.messages ?? session.agent.state.messages,
584
594
  })
585
595
  }
586
- const timeout = setTimeout(() => {
587
- cleanup(handler, timeout)
588
- resolve({ ok: false, aborted: false, error: '执行超时', messages: session.agent.state.messages })
589
- }, Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000)))
590
596
  eventBus?.on('agent_event', handler)
591
597
  })
592
598
 
599
+ const runPromise = (async () => {
600
+ try {
601
+ await session.agent.continue()
602
+ } catch (continueError) {
603
+ if (continueError?.message !== 'Request was aborted' && continueError?.message !== 'Scheduled task aborted') {
604
+ throw continueError
605
+ }
606
+ }
607
+ return finished
608
+ })()
609
+ const timeoutPromise = new Promise((resolve) => {
610
+ timeout = setTimeout(() => resolve({ timedOut: true }), runtimeLimitMs)
611
+ })
612
+
613
+ let result
593
614
  try {
594
- await session.agent.continue()
595
- } catch (continueError) {
596
- if (continueError?.message === 'Request was aborted' || continueError?.message === 'Scheduled task aborted') {
597
- settled = true
615
+ const outcome = await Promise.race([
616
+ runPromise.then((value) => ({ result: value })),
617
+ timeoutPromise,
618
+ ])
619
+ if (outcome.timedOut) {
620
+ timedOut = true
621
+ const abortPromise = Promise.resolve()
622
+ .then(() => abortRun(sessionId))
623
+ .catch((error) => {
624
+ logger.warn(`Failed to abort timed out scheduled task ${task.id}:`, error)
625
+ })
626
+ await Promise.race([
627
+ abortPromise,
628
+ new Promise((resolve) => setTimeout(resolve, 1000)),
629
+ ])
630
+ await Promise.race([
631
+ runPromise.catch(() => undefined),
632
+ new Promise((resolve) => setTimeout(resolve, 1000)),
633
+ ])
634
+ result = { ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages }
598
635
  } else {
599
- throw continueError
636
+ result = outcome.result
600
637
  }
638
+ } finally {
639
+ clearTimeout(timeout)
640
+ eventBus?.removeListener('agent_event', handler)
641
+ if (timedOut) resolveFinished?.({ ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages })
601
642
  }
602
- const result = await finished
603
- if (result.aborted) settled = true
643
+ settled = true
604
644
  const finishedAt = new Date().toISOString()
605
645
  const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
606
646
  const aiResult = result.ok ? latestAssistantText(result.messages) : ''
@@ -634,7 +674,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
634
674
  inputContent: run.inputContent ?? latestTask.instruction,
635
675
  aiResult: result.ok ? aiResult : undefined,
636
676
  result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
637
- errorMessage: result.aborted ? '已暂停执行' : result.error,
677
+ errorMessage: result.error,
638
678
  sessionId,
639
679
  agentId: executionAgent?.id || latestTask.agentId || null,
640
680
  agentLabel: executionAgent?.label || null,
@@ -651,7 +691,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
651
691
  sessionId,
652
692
  status: result.aborted ? 'failed' : (result.ok ? 'success' : 'failed'),
653
693
  result: aiResult,
654
- errorMessage: result.aborted ? '已暂停执行' : result.error,
694
+ errorMessage: result.error,
655
695
  })
656
696
  } catch (error) {
657
697
  const finishedAt = new Date().toISOString()
@@ -9,12 +9,17 @@ import { logger } from '../utils/logger.mjs'
9
9
  import { openPathInFileManager, openPathInIDEA, openPathInVSCode } from '../utils/platform.mjs'
10
10
  import {
11
11
  assertSafeWorkspacePath,
12
+ createWorkspacePathValidator,
12
13
  resolveWorkspacePath,
13
14
  toWorkspaceRelative,
14
15
  } from '../utils/workspace.mjs'
15
16
 
16
17
  const MAX_PREVIEW_BYTES = 50 * 1024 * 1024
17
18
  const MAX_STATIC_PREVIEW_BYTES = 50 * 1024 * 1024
19
+ const MAX_GIT_LINE_COUNT_FILES = 100
20
+ const MAX_GIT_LINE_COUNT_FILE_BYTES = 1024 * 1024
21
+ const MAX_GIT_LINE_COUNT_TOTAL_BYTES = 10 * 1024 * 1024
22
+ const GIT_LINE_COUNT_CONCURRENCY = 6
18
23
  const PREVIEW_ALLOWED_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.ico', '.txt', '.md'])
19
24
  const MAX_TREE_NODES = 50000
20
25
  const SKIP_DIRS = new Set(['.git', 'node_modules'])
@@ -294,16 +299,78 @@ async function collectNumstat(context) {
294
299
  return map
295
300
  }
296
301
 
297
- // 未跟踪文件不在 numstat 中,按工作区文件行数估算新增行
298
- async function countWorkspaceLines(context, relativePath) {
302
+ async function readUtf8FileAtMost(fullPath, maxBytes) {
303
+ if (maxBytes === 0) return ''
304
+ const handle = await fs.open(fullPath, 'r')
299
305
  try {
300
- const { content } = await readWorkspaceTextFile(context, relativePath)
301
- return countTextLines(content)
302
- } catch {
303
- return undefined
306
+ const buffer = Buffer.allocUnsafe(maxBytes)
307
+ let offset = 0
308
+ while (offset < maxBytes) {
309
+ const { bytesRead } = await handle.read(buffer, offset, maxBytes - offset, offset)
310
+ if (bytesRead === 0) break
311
+ offset += bytesRead
312
+ }
313
+ return buffer.subarray(0, offset).toString('utf8')
314
+ } finally {
315
+ await handle.close()
304
316
  }
305
317
  }
306
318
 
319
+ async function poolMap(items, fn, concurrency) {
320
+ const results = new Array(items.length)
321
+ let cursor = 0
322
+ async function worker() {
323
+ while (cursor < items.length) {
324
+ const index = cursor
325
+ cursor += 1
326
+ results[index] = await fn(items[index], index)
327
+ }
328
+ }
329
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()))
330
+ return results
331
+ }
332
+
333
+ // 未跟踪文件不在 numstat 中,在有界预算内按工作区文件行数估算新增行
334
+ async function collectWorkspaceLineCounts(context, files) {
335
+ const candidates = files
336
+ .filter((file) => file.status === 'untracked' || file.status === 'added')
337
+ .slice(0, MAX_GIT_LINE_COUNT_FILES)
338
+ if (candidates.length === 0) return new Map()
339
+
340
+ const validateWorkspacePath = await createWorkspacePathValidator(context)
341
+ const inspected = await poolMap(candidates, async (file) => {
342
+ try {
343
+ const fullPath = resolveWorkspacePath(file.path, context)
344
+ await validateWorkspacePath(fullPath, { allowSensitive: true })
345
+ const stat = await fs.stat(fullPath)
346
+ if (!stat.isFile() || stat.size > MAX_GIT_LINE_COUNT_FILE_BYTES) return null
347
+ return { file, fullPath, size: stat.size }
348
+ } catch {
349
+ return null
350
+ }
351
+ }, GIT_LINE_COUNT_CONCURRENCY)
352
+
353
+ let totalBytes = 0
354
+ const selected = []
355
+ for (const entry of inspected) {
356
+ if (!entry || totalBytes + entry.size > MAX_GIT_LINE_COUNT_TOTAL_BYTES) continue
357
+ totalBytes += entry.size
358
+ selected.push(entry)
359
+ }
360
+
361
+ const counts = await poolMap(selected, async ({ file, fullPath, size }) => {
362
+ try {
363
+ const stat = await fs.stat(fullPath)
364
+ if (!stat.isFile() || stat.size > size) return null
365
+ const content = await readUtf8FileAtMost(fullPath, size)
366
+ return [file.path, countTextLines(content)]
367
+ } catch {
368
+ return null
369
+ }
370
+ }, GIT_LINE_COUNT_CONCURRENCY)
371
+ return new Map(counts.filter(Boolean))
372
+ }
373
+
307
374
  export async function listGitStatus(context) {
308
375
  if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
309
376
  const result = await git(
@@ -312,17 +379,19 @@ export async function listGitStatus(context) {
312
379
  )
313
380
  const files = parseGitStatus(result.stdout)
314
381
  const numstat = await collectNumstat(context)
382
+ const fallbackFiles = files.filter((file) => !numstat.has(file.path))
383
+ const workspaceLineCounts = await collectWorkspaceLineCounts(context, fallbackFiles)
315
384
  for (const file of files) {
316
385
  const entry = numstat.get(file.path)
317
386
  if (entry) {
318
387
  file.additions = entry.additions
319
388
  file.deletions = entry.deletions
320
- } else if (file.status === 'untracked' || file.status === 'added') {
321
- const count = await countWorkspaceLines(context, file.path)
322
- if (typeof count === 'number') {
323
- file.additions = count
324
- file.deletions = 0
325
- }
389
+ continue
390
+ }
391
+ const count = workspaceLineCounts.get(file.path)
392
+ if (typeof count === 'number') {
393
+ file.additions = count
394
+ file.deletions = 0
326
395
  }
327
396
  }
328
397
  const head = await currentGitHead(context.workspaceRoot)
@@ -601,7 +670,7 @@ async function readWorkspaceTextFile(context, relativePath) {
601
670
  return { content: buffer.toString('utf8'), size: stat.size, path: toWorkspaceRelative(file, context) }
602
671
  }
603
672
 
604
- async function buildTreeForDirectory(dir, context, counter) {
673
+ async function buildTreeForDirectory(dir, context, counter, validateWorkspacePath) {
605
674
  const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
606
675
  const nodes = []
607
676
  const sortedEntries = entries.sort((left, right) => {
@@ -616,20 +685,20 @@ async function buildTreeForDirectory(dir, context, counter) {
616
685
  if (entry.isDirectory()) {
617
686
  if (SKIP_DIRS.has(entry.name)) continue
618
687
  try {
619
- await assertSafeWorkspacePath(fullPath, context, { allowSensitive: true })
688
+ await validateWorkspacePath(fullPath, { allowSensitive: true })
620
689
  counter.count += 1
621
690
  nodes.push({
622
691
  name: entry.name,
623
692
  path: relativePath,
624
693
  type: 'directory',
625
- children: await buildTreeForDirectory(fullPath, context, counter),
694
+ children: await buildTreeForDirectory(fullPath, context, counter, validateWorkspacePath),
626
695
  })
627
696
  } catch {
628
697
  // Skip directories that cannot be safely resolved.
629
698
  }
630
699
  } else if (entry.isFile()) {
631
700
  try {
632
- await assertSafeWorkspacePath(fullPath, context, { allowSensitive: true })
701
+ await validateWorkspacePath(fullPath, { allowSensitive: true })
633
702
  counter.count += 1
634
703
  nodes.push({ name: entry.name, path: relativePath, type: 'file' })
635
704
  } catch {
@@ -642,7 +711,8 @@ async function buildTreeForDirectory(dir, context, counter) {
642
711
 
643
712
  async function handleWorkspaceTree(req, res, url) {
644
713
  const context = await projectContextFromUrl(url)
645
- const tree = await buildTreeForDirectory(context.workspaceRoot, context, { count: 0 })
714
+ const validateWorkspacePath = await createWorkspacePathValidator(context)
715
+ const tree = await buildTreeForDirectory(context.workspaceRoot, context, { count: 0 }, validateWorkspacePath)
646
716
  sendJson(res, 200, { root: context.project.name, tree })
647
717
  }
648
718
 
@@ -90,6 +90,7 @@ function parseCronField(field, min, max) {
90
90
  for (const part of field.split(',')) {
91
91
  if (/^\*\/\d+$/.test(part)) {
92
92
  const step = Number(part.slice(2))
93
+ if (!Number.isInteger(step) || step <= 0) return null
93
94
  for (let value = min; value <= max; value += step) values.add(value)
94
95
  } else if (/^\d+-\d+$/.test(part)) {
95
96
  const [start, end] = part.split('-').map(Number)
@@ -102,25 +103,37 @@ function parseCronField(field, min, max) {
102
103
  return { any: false, values: [...values] }
103
104
  }
104
105
 
105
- export function cronMatches(date, cronExpression) {
106
+ function parseCronExpression(cronExpression) {
106
107
  const fields = String(cronExpression || '').trim().split(/\s+/)
107
- if (fields.length !== 5) return false
108
- const checks = [
109
- [date.getMinutes(), parseCronField(fields[0], 0, 59)],
110
- [date.getHours(), parseCronField(fields[1], 0, 23)],
111
- [date.getDate(), parseCronField(fields[2], 1, 31)],
112
- [date.getMonth() + 1, parseCronField(fields[3], 1, 12)],
113
- [date.getDay(), parseCronField(fields[4], 0, 6)],
108
+ if (fields.length !== 5) return null
109
+ const rules = [
110
+ parseCronField(fields[0], 0, 59),
111
+ parseCronField(fields[1], 0, 23),
112
+ parseCronField(fields[2], 1, 31),
113
+ parseCronField(fields[3], 1, 12),
114
+ parseCronField(fields[4], 0, 6),
114
115
  ]
115
- return checks.every(([value, rule]) => rule.any || rule.values.includes(value))
116
+ return rules.every(Boolean) ? rules : null
117
+ }
118
+
119
+ function cronRulesMatch(date, rules) {
120
+ const values = [date.getMinutes(), date.getHours(), date.getDate(), date.getMonth() + 1, date.getDay()]
121
+ return rules.every((rule, index) => rule.any || rule.values.includes(values[index]))
122
+ }
123
+
124
+ export function cronMatches(date, cronExpression) {
125
+ const rules = parseCronExpression(cronExpression)
126
+ return rules ? cronRulesMatch(date, rules) : false
116
127
  }
117
128
 
118
129
  export function nextCronRun(cronExpression, base = new Date()) {
130
+ const rules = parseCronExpression(cronExpression)
131
+ if (!rules) return null
119
132
  const cursor = new Date(base.getTime() + minuteMs)
120
133
  cursor.setSeconds(0, 0)
121
134
  const maxChecks = 366 * 24 * 60
122
135
  for (let index = 0; index < maxChecks; index += 1) {
123
- if (cronMatches(cursor, cronExpression)) return cursor
136
+ if (cronRulesMatch(cursor, rules)) return cursor
124
137
  cursor.setMinutes(cursor.getMinutes() + 1)
125
138
  }
126
139
  return null
@@ -75,15 +75,13 @@ async function realpathNearestExistingParent(inputPath) {
75
75
  }
76
76
  }
77
77
 
78
- export async function assertSafeWorkspacePath(fullPath, context, options = {}) {
78
+ async function assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal, options = {}) {
79
79
  if (!options.allowSensitive && isSensitiveWorkspacePath(fullPath, context)) {
80
80
  const error = new Error(`Access to sensitive path is blocked: ${toWorkspaceRelative(fullPath, context)}`)
81
81
  error.statusCode = 403
82
82
  throw error
83
83
  }
84
84
 
85
- const workspaceRoot = getToolWorkspaceRoot(context)
86
- const workspaceReal = await fs.realpath(workspaceRoot)
87
85
  let targetReal
88
86
  try {
89
87
  targetReal = await fs.realpath(fullPath)
@@ -104,6 +102,16 @@ export async function assertSafeWorkspacePath(fullPath, context, options = {}) {
104
102
  }
105
103
  }
106
104
 
105
+ export async function createWorkspacePathValidator(context) {
106
+ const workspaceReal = await fs.realpath(getToolWorkspaceRoot(context))
107
+ return (fullPath, options = {}) => assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal, options)
108
+ }
109
+
110
+ export async function assertSafeWorkspacePath(fullPath, context, options = {}) {
111
+ const validateWorkspacePath = await createWorkspacePathValidator(context)
112
+ return validateWorkspacePath(fullPath, options)
113
+ }
114
+
107
115
  export function truncateText(text, maxChars = 50000) {
108
116
  if (text.length <= maxChars) return text
109
117
  return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`
@@ -134,12 +142,25 @@ export async function assertDirectory(dir) {
134
142
  const SIZE_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'dist-ssr', '.vite', '.cache', '.next', '.nuxt', '__pycache__', '.venv', 'venv'])
135
143
  const directorySizeCache = new Map()
136
144
  const DIRECTORY_SIZE_CACHE_TTL_MS = 10_000
145
+ const DIRECTORY_SIZE_CACHE_MAX_ENTRIES = 10_000
146
+
147
+ function pruneDirectorySizeCache(now = Date.now()) {
148
+ for (const [key, cached] of directorySizeCache) {
149
+ if (now - cached.ts >= DIRECTORY_SIZE_CACHE_TTL_MS) directorySizeCache.delete(key)
150
+ }
151
+ while (directorySizeCache.size > DIRECTORY_SIZE_CACHE_MAX_ENTRIES) {
152
+ const oldestKey = directorySizeCache.keys().next().value
153
+ if (oldestKey === undefined) break
154
+ directorySizeCache.delete(oldestKey)
155
+ }
156
+ }
137
157
 
138
158
  export async function directorySize(dir) {
139
159
  try {
140
160
  const now = Date.now()
141
161
  const cached = directorySizeCache.get(dir)
142
162
  if (cached && now - cached.ts < DIRECTORY_SIZE_CACHE_TTL_MS) return cached.size
163
+ if (cached) directorySizeCache.delete(dir)
143
164
 
144
165
  const entries = await fs.readdir(dir, { withFileTypes: true })
145
166
  const sizes = await Promise.all(entries.map(async (entry) => {
@@ -151,6 +172,7 @@ export async function directorySize(dir) {
151
172
  }))
152
173
  const size = sizes.reduce((sum, value) => sum + value, 0)
153
174
  directorySizeCache.set(dir, { size, ts: now })
175
+ if (directorySizeCache.size > DIRECTORY_SIZE_CACHE_MAX_ENTRIES) pruneDirectorySizeCache(now)
154
176
  return size
155
177
  } catch {
156
178
  return 0
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{E as t,Ft as n,Mt as r,Ot as i,a,c as o,it as ee}from"./icons-pPRMD2tE.js";import{i as s,n as c}from"./react-vendor-CLbWF1Oy.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-BSXpUDCq.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)(r,{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)(i,{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)(ee,{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)(t,{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)(a,{className:`size-3.5`}),d(`delete`)]})]}),document.body):null]})}export{T as AgentProfilesPage};