@shawnstack/quickforge 1.7.1 → 1.7.3

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 (46) hide show
  1. package/README.md +1 -0
  2. package/bin/quickforge.mjs +2 -0
  3. package/dist/assets/AgentProfilesPage-D7pbg8nm.js +1 -0
  4. package/dist/assets/ChatPanelHost-wfGCaCzD.js +288 -0
  5. package/dist/assets/{PluginsPage-CKyWhlCo.js → PluginsPage-NAGroBm3.js} +1 -1
  6. package/dist/assets/ScheduledTasksPage-Bp0MNfQD.js +2 -0
  7. package/dist/assets/{SettingsWorkspacePage-DwqEnUmX.js → SettingsWorkspacePage-BtJZMIsZ.js} +425 -344
  8. package/dist/assets/SharedConversationPage-Ce3KoVZt.js +1 -0
  9. package/dist/assets/{TerminalDock-B9xKnimU.js → TerminalDock-4eUuXP7X.js} +2 -2
  10. package/dist/assets/WorkspaceInspector-kM3BRPjY.js +13 -0
  11. package/dist/assets/icons-BP8YOS-Z.js +1 -0
  12. package/dist/assets/index-C6k5taeb.js +66 -0
  13. package/dist/assets/index-CzN8NSKC.css +3 -0
  14. package/dist/assets/mcp-servers-dialog-BQbFywVL.js +5 -0
  15. package/dist/assets/{monaco-CPwJMUsl.js → monaco-BTsVCDWS.js} +1 -1
  16. package/dist/assets/{react-vendor-CLbWF1Oy.js → react-vendor-Dr5xvL-e.js} +1 -1
  17. package/dist/assets/{skills-dialog-D18mxNyW.js → skills-dialog-REBeTFSH.js} +1 -1
  18. package/dist/index.html +6 -6
  19. package/package.json +4 -1
  20. package/server/acp/server.mjs +19 -7
  21. package/server/ai-http-logger.mjs +6 -6
  22. package/server/ai-provider-options.mjs +8 -0
  23. package/server/index.mjs +7 -2
  24. package/server/mcp/registry.mjs +54 -14
  25. package/server/network-proxy.mjs +384 -0
  26. package/server/plugins/loader.mjs +9 -1
  27. package/server/plugins/registry.mjs +32 -10
  28. package/server/public-api.mjs +4 -0
  29. package/server/routes/agent-profiles.mjs +2 -0
  30. package/server/routes/mcp.mjs +0 -5
  31. package/server/routes/models.mjs +1 -0
  32. package/server/routes/scheduled-tasks.mjs +74 -32
  33. package/server/routes/system.mjs +26 -0
  34. package/server/routes/workspace.mjs +89 -17
  35. package/server/session-utils.mjs +2 -0
  36. package/server/utils/scheduled-tasks.mjs +23 -10
  37. package/server/utils/workspace.mjs +25 -3
  38. package/dist/assets/AgentProfilesPage-__uY0AvK.js +0 -1
  39. package/dist/assets/ChatPanelHost-DIs_vWFX.js +0 -291
  40. package/dist/assets/ScheduledTasksPage-KtJoeJYt.js +0 -2
  41. package/dist/assets/SharedConversationPage-yujCRsbe.js +0 -1
  42. package/dist/assets/WorkspaceInspector-CE6RD6Ys.js +0 -13
  43. package/dist/assets/icons-pPRMD2tE.js +0 -1
  44. package/dist/assets/index-BSXpUDCq.js +0 -63
  45. package/dist/assets/index-DpO7jEGP.css +0 -3
  46. package/dist/assets/mcp-servers-dialog-CaQTvGiW.js +0 -5
@@ -1,7 +1,8 @@
1
1
  import { streamSimple } from '@earendil-works/pi-ai/compat'
2
+ import { DEFAULT_AI_MAX_RETRIES } from '../ai-provider-options.mjs'
2
3
  import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
3
4
  import { readStore, atomicUpdate } from '../storage.mjs'
4
- import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'
5
+ import { createAgent, getSessionEventBus, agentEvents, persistSessionState, abortRun } from '../agent-manager.mjs'
5
6
  import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
6
7
  import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
7
8
  import { logger } from '../utils/logger.mjs'
@@ -165,6 +166,7 @@ async function parseScheduledTaskInstructionWithAi(instruction, model, thinkingL
165
166
  maxTokens: 600,
166
167
  temperature: 0,
167
168
  reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
169
+ maxRetries: DEFAULT_AI_MAX_RETRIES,
168
170
  maxRetryDelayMs: 60000,
169
171
  },
170
172
  )
@@ -436,21 +438,30 @@ async function updateTask(taskId, updater) {
436
438
  return updated
437
439
  }
438
440
 
441
+ function recurringTaskStatusRepair(task, now) {
442
+ if (task?.status !== 'completed' || !isRecurringTask(task)) return null
443
+ const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > now.getTime()
444
+ ? task.nextRunAt
445
+ : calculateNextRun(task, now)
446
+ if (!nextRunAt) return null
447
+ return {
448
+ ...task,
449
+ status: 'enabled',
450
+ nextRunAt,
451
+ scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
452
+ updatedAt: now.toISOString(),
453
+ }
454
+ }
455
+
439
456
  async function repairRecurringTaskStatuses() {
457
+ const now = new Date()
458
+ const snapshot = await readStore(STORE)
459
+ if (!Object.values(snapshot).some((task) => recurringTaskStatusRepair(task, now))) return
460
+
440
461
  await atomicUpdate(STORE, (data) => {
441
462
  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
- }
463
+ const repaired = recurringTaskStatusRepair(task, now)
464
+ if (repaired) data[taskId] = repaired
454
465
  }
455
466
  return data
456
467
  })
@@ -566,14 +577,15 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
566
577
  onStarted?.({ taskId: task.id, runId, sessionId })
567
578
 
568
579
  const eventBus = getSessionEventBus(sessionId)
580
+ const runtimeLimitMs = Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000))
581
+ let timeout = null
582
+ let handler = null
583
+ let timedOut = false
584
+ let resolveFinished
569
585
  const finished = new Promise((resolve) => {
570
- const cleanup = (handler, timeout) => {
571
- clearTimeout(timeout)
572
- eventBus?.removeListener('agent_event', handler)
573
- }
574
- const handler = (event) => {
586
+ resolveFinished = resolve
587
+ handler = (event) => {
575
588
  if (event.type !== 'agent_end') return
576
- cleanup(handler, timeout)
577
589
  const errorMessage = event.errorMessage || session.agent.state.errorMessage
578
590
  const aborted = session.status === 'aborted' || session.agent.state.messages.some((message) => message?.role === 'assistant' && message?.stopReason === 'aborted')
579
591
  resolve({
@@ -583,24 +595,54 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
583
595
  messages: event.messages ?? session.agent.state.messages,
584
596
  })
585
597
  }
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
598
  eventBus?.on('agent_event', handler)
591
599
  })
592
600
 
601
+ const runPromise = (async () => {
602
+ try {
603
+ await session.agent.continue()
604
+ } catch (continueError) {
605
+ if (continueError?.message !== 'Request was aborted' && continueError?.message !== 'Scheduled task aborted') {
606
+ throw continueError
607
+ }
608
+ }
609
+ return finished
610
+ })()
611
+ const timeoutPromise = new Promise((resolve) => {
612
+ timeout = setTimeout(() => resolve({ timedOut: true }), runtimeLimitMs)
613
+ })
614
+
615
+ let result
593
616
  try {
594
- await session.agent.continue()
595
- } catch (continueError) {
596
- if (continueError?.message === 'Request was aborted' || continueError?.message === 'Scheduled task aborted') {
597
- settled = true
617
+ const outcome = await Promise.race([
618
+ runPromise.then((value) => ({ result: value })),
619
+ timeoutPromise,
620
+ ])
621
+ if (outcome.timedOut) {
622
+ timedOut = true
623
+ const abortPromise = Promise.resolve()
624
+ .then(() => abortRun(sessionId))
625
+ .catch((error) => {
626
+ logger.warn(`Failed to abort timed out scheduled task ${task.id}:`, error)
627
+ })
628
+ await Promise.race([
629
+ abortPromise,
630
+ new Promise((resolve) => setTimeout(resolve, 1000)),
631
+ ])
632
+ await Promise.race([
633
+ runPromise.catch(() => undefined),
634
+ new Promise((resolve) => setTimeout(resolve, 1000)),
635
+ ])
636
+ result = { ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages }
598
637
  } else {
599
- throw continueError
638
+ result = outcome.result
600
639
  }
640
+ } finally {
641
+ clearTimeout(timeout)
642
+ eventBus?.removeListener('agent_event', handler)
643
+ if (timedOut) resolveFinished?.({ ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages })
601
644
  }
602
- const result = await finished
603
- if (result.aborted) settled = true
645
+ settled = true
604
646
  const finishedAt = new Date().toISOString()
605
647
  const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
606
648
  const aiResult = result.ok ? latestAssistantText(result.messages) : ''
@@ -634,7 +676,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
634
676
  inputContent: run.inputContent ?? latestTask.instruction,
635
677
  aiResult: result.ok ? aiResult : undefined,
636
678
  result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
637
- errorMessage: result.aborted ? '已暂停执行' : result.error,
679
+ errorMessage: result.error,
638
680
  sessionId,
639
681
  agentId: executionAgent?.id || latestTask.agentId || null,
640
682
  agentLabel: executionAgent?.label || null,
@@ -651,7 +693,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
651
693
  sessionId,
652
694
  status: result.aborted ? 'failed' : (result.ok ? 'success' : 'failed'),
653
695
  result: aiResult,
654
- errorMessage: result.aborted ? '已暂停执行' : result.error,
696
+ errorMessage: result.error,
655
697
  })
656
698
  } catch (error) {
657
699
  const finishedAt = new Date().toISOString()
@@ -70,6 +70,32 @@ export async function handleSystemApi(req, res, url, context) {
70
70
  }
71
71
  }
72
72
 
73
+ if (req.method === 'GET' && url.pathname === '/api/system/network-proxy') {
74
+ sendJson(res, 200, await context.getNetworkProxyConfig())
75
+ return
76
+ }
77
+
78
+ if (req.method === 'PUT' && url.pathname === '/api/system/network-proxy') {
79
+ if (!context.isLocalRequest) {
80
+ const error = new Error('Network proxy settings can only be changed from this computer')
81
+ error.statusCode = 403
82
+ throw error
83
+ }
84
+ const body = await readJsonBody(req, 64 * 1024) || {}
85
+ sendJson(res, 200, await context.updateNetworkProxyConfig(body))
86
+ return
87
+ }
88
+
89
+ if (req.method === 'POST' && url.pathname === '/api/system/network-proxy/refresh') {
90
+ if (!context.isLocalRequest) {
91
+ const error = new Error('System proxy can only be refreshed from this computer')
92
+ error.statusCode = 403
93
+ throw error
94
+ }
95
+ sendJson(res, 200, await context.refreshSystemProxy())
96
+ return
97
+ }
98
+
73
99
  if (req.method === 'GET' && url.pathname === '/api/system/network') {
74
100
  sendJson(res, 200, {
75
101
  host: context.host,
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawn } from 'node:child_process'
4
4
  import { streamSimple } from '@earendil-works/pi-ai/compat'
5
+ import { DEFAULT_AI_MAX_RETRIES } from '../ai-provider-options.mjs'
5
6
  import { sendJson, readJsonBody } from '../utils/response.mjs'
6
7
  import { projectContextFromId } from '../project-config.mjs'
7
8
  import { readStore } from '../storage.mjs'
@@ -9,12 +10,17 @@ import { logger } from '../utils/logger.mjs'
9
10
  import { openPathInFileManager, openPathInIDEA, openPathInVSCode } from '../utils/platform.mjs'
10
11
  import {
11
12
  assertSafeWorkspacePath,
13
+ createWorkspacePathValidator,
12
14
  resolveWorkspacePath,
13
15
  toWorkspaceRelative,
14
16
  } from '../utils/workspace.mjs'
15
17
 
16
18
  const MAX_PREVIEW_BYTES = 50 * 1024 * 1024
17
19
  const MAX_STATIC_PREVIEW_BYTES = 50 * 1024 * 1024
20
+ const MAX_GIT_LINE_COUNT_FILES = 100
21
+ const MAX_GIT_LINE_COUNT_FILE_BYTES = 1024 * 1024
22
+ const MAX_GIT_LINE_COUNT_TOTAL_BYTES = 10 * 1024 * 1024
23
+ const GIT_LINE_COUNT_CONCURRENCY = 6
18
24
  const PREVIEW_ALLOWED_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.ico', '.txt', '.md'])
19
25
  const MAX_TREE_NODES = 50000
20
26
  const SKIP_DIRS = new Set(['.git', 'node_modules'])
@@ -294,16 +300,78 @@ async function collectNumstat(context) {
294
300
  return map
295
301
  }
296
302
 
297
- // 未跟踪文件不在 numstat 中,按工作区文件行数估算新增行
298
- async function countWorkspaceLines(context, relativePath) {
303
+ async function readUtf8FileAtMost(fullPath, maxBytes) {
304
+ if (maxBytes === 0) return ''
305
+ const handle = await fs.open(fullPath, 'r')
299
306
  try {
300
- const { content } = await readWorkspaceTextFile(context, relativePath)
301
- return countTextLines(content)
302
- } catch {
303
- return undefined
307
+ const buffer = Buffer.allocUnsafe(maxBytes)
308
+ let offset = 0
309
+ while (offset < maxBytes) {
310
+ const { bytesRead } = await handle.read(buffer, offset, maxBytes - offset, offset)
311
+ if (bytesRead === 0) break
312
+ offset += bytesRead
313
+ }
314
+ return buffer.subarray(0, offset).toString('utf8')
315
+ } finally {
316
+ await handle.close()
304
317
  }
305
318
  }
306
319
 
320
+ async function poolMap(items, fn, concurrency) {
321
+ const results = new Array(items.length)
322
+ let cursor = 0
323
+ async function worker() {
324
+ while (cursor < items.length) {
325
+ const index = cursor
326
+ cursor += 1
327
+ results[index] = await fn(items[index], index)
328
+ }
329
+ }
330
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()))
331
+ return results
332
+ }
333
+
334
+ // 未跟踪文件不在 numstat 中,在有界预算内按工作区文件行数估算新增行
335
+ async function collectWorkspaceLineCounts(context, files) {
336
+ const candidates = files
337
+ .filter((file) => file.status === 'untracked' || file.status === 'added')
338
+ .slice(0, MAX_GIT_LINE_COUNT_FILES)
339
+ if (candidates.length === 0) return new Map()
340
+
341
+ const validateWorkspacePath = await createWorkspacePathValidator(context)
342
+ const inspected = await poolMap(candidates, async (file) => {
343
+ try {
344
+ const fullPath = resolveWorkspacePath(file.path, context)
345
+ await validateWorkspacePath(fullPath, { allowSensitive: true })
346
+ const stat = await fs.stat(fullPath)
347
+ if (!stat.isFile() || stat.size > MAX_GIT_LINE_COUNT_FILE_BYTES) return null
348
+ return { file, fullPath, size: stat.size }
349
+ } catch {
350
+ return null
351
+ }
352
+ }, GIT_LINE_COUNT_CONCURRENCY)
353
+
354
+ let totalBytes = 0
355
+ const selected = []
356
+ for (const entry of inspected) {
357
+ if (!entry || totalBytes + entry.size > MAX_GIT_LINE_COUNT_TOTAL_BYTES) continue
358
+ totalBytes += entry.size
359
+ selected.push(entry)
360
+ }
361
+
362
+ const counts = await poolMap(selected, async ({ file, fullPath, size }) => {
363
+ try {
364
+ const stat = await fs.stat(fullPath)
365
+ if (!stat.isFile() || stat.size > size) return null
366
+ const content = await readUtf8FileAtMost(fullPath, size)
367
+ return [file.path, countTextLines(content)]
368
+ } catch {
369
+ return null
370
+ }
371
+ }, GIT_LINE_COUNT_CONCURRENCY)
372
+ return new Map(counts.filter(Boolean))
373
+ }
374
+
307
375
  export async function listGitStatus(context) {
308
376
  if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
309
377
  const result = await git(
@@ -312,17 +380,19 @@ export async function listGitStatus(context) {
312
380
  )
313
381
  const files = parseGitStatus(result.stdout)
314
382
  const numstat = await collectNumstat(context)
383
+ const fallbackFiles = files.filter((file) => !numstat.has(file.path))
384
+ const workspaceLineCounts = await collectWorkspaceLineCounts(context, fallbackFiles)
315
385
  for (const file of files) {
316
386
  const entry = numstat.get(file.path)
317
387
  if (entry) {
318
388
  file.additions = entry.additions
319
389
  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
- }
390
+ continue
391
+ }
392
+ const count = workspaceLineCounts.get(file.path)
393
+ if (typeof count === 'number') {
394
+ file.additions = count
395
+ file.deletions = 0
326
396
  }
327
397
  }
328
398
  const head = await currentGitHead(context.workspaceRoot)
@@ -561,6 +631,7 @@ ${trimForPrompt(worktreeDiff)}`
561
631
  maxTokens: 500,
562
632
  temperature: 0,
563
633
  reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
634
+ maxRetries: DEFAULT_AI_MAX_RETRIES,
564
635
  maxRetryDelayMs: 60000,
565
636
  },
566
637
  )
@@ -601,7 +672,7 @@ async function readWorkspaceTextFile(context, relativePath) {
601
672
  return { content: buffer.toString('utf8'), size: stat.size, path: toWorkspaceRelative(file, context) }
602
673
  }
603
674
 
604
- async function buildTreeForDirectory(dir, context, counter) {
675
+ async function buildTreeForDirectory(dir, context, counter, validateWorkspacePath) {
605
676
  const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
606
677
  const nodes = []
607
678
  const sortedEntries = entries.sort((left, right) => {
@@ -616,20 +687,20 @@ async function buildTreeForDirectory(dir, context, counter) {
616
687
  if (entry.isDirectory()) {
617
688
  if (SKIP_DIRS.has(entry.name)) continue
618
689
  try {
619
- await assertSafeWorkspacePath(fullPath, context, { allowSensitive: true })
690
+ await validateWorkspacePath(fullPath, { allowSensitive: true })
620
691
  counter.count += 1
621
692
  nodes.push({
622
693
  name: entry.name,
623
694
  path: relativePath,
624
695
  type: 'directory',
625
- children: await buildTreeForDirectory(fullPath, context, counter),
696
+ children: await buildTreeForDirectory(fullPath, context, counter, validateWorkspacePath),
626
697
  })
627
698
  } catch {
628
699
  // Skip directories that cannot be safely resolved.
629
700
  }
630
701
  } else if (entry.isFile()) {
631
702
  try {
632
- await assertSafeWorkspacePath(fullPath, context, { allowSensitive: true })
703
+ await validateWorkspacePath(fullPath, { allowSensitive: true })
633
704
  counter.count += 1
634
705
  nodes.push({ name: entry.name, path: relativePath, type: 'file' })
635
706
  } catch {
@@ -642,7 +713,8 @@ async function buildTreeForDirectory(dir, context, counter) {
642
713
 
643
714
  async function handleWorkspaceTree(req, res, url) {
644
715
  const context = await projectContextFromUrl(url)
645
- const tree = await buildTreeForDirectory(context.workspaceRoot, context, { count: 0 })
716
+ const validateWorkspacePath = await createWorkspacePathValidator(context)
717
+ const tree = await buildTreeForDirectory(context.workspaceRoot, context, { count: 0 }, validateWorkspacePath)
646
718
  sendJson(res, 200, { root: context.project.name, tree })
647
719
  }
648
720
 
@@ -1,4 +1,5 @@
1
1
  import { streamSimple } from '@earendil-works/pi-ai/compat'
2
+ import { DEFAULT_AI_MAX_RETRIES } from './ai-provider-options.mjs'
2
3
  import { buildInstructionsPayload, projectContextFromId } from './project-config.mjs'
3
4
  import { composeSystemPrompt } from './system-prompt.mjs'
4
5
  import { listSubagentProfiles } from './agent-profiles.mjs'
@@ -99,6 +100,7 @@ export async function generateAiTitle(messages, model, thinkingLevel, getApiKey)
99
100
  maxTokens: 160,
100
101
  temperature: 0.2,
101
102
  reasoning: thinkingLevel === 'off' ? undefined : 'medium',
103
+ maxRetries: DEFAULT_AI_MAX_RETRIES,
102
104
  maxRetryDelayMs: 60000,
103
105
  },
104
106
  )
@@ -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};