@shawnstack/quickforge 1.7.0 → 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 (37) 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-B8bHMwfv.js → PluginsPage-DEMqwhOA.js} +1 -1
  4. package/dist/assets/ScheduledTasksPage-DTP_gedp.js +2 -0
  5. package/dist/assets/{SettingsWorkspacePage-BOP9CtoI.js → SettingsWorkspacePage-Dr9zT5Bs.js} +112 -112
  6. package/dist/assets/SharedConversationPage-op3DRwcw.js +1 -0
  7. package/dist/assets/TerminalDock-BL_UovwU.js +2 -0
  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-C13M09wD.js → monaco-BTsVCDWS.js} +1 -1
  14. package/dist/assets/{react-vendor-2RKYr-A4.js → react-vendor-Dr5xvL-e.js} +1 -1
  15. package/dist/assets/{skills-dialog-MpoxntiV.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/backup.mjs +79 -72
  23. package/server/routes/mcp.mjs +0 -5
  24. package/server/routes/scheduled-tasks.mjs +72 -32
  25. package/server/routes/workspace.mjs +171 -49
  26. package/server/utils/scheduled-tasks.mjs +23 -10
  27. package/server/utils/workspace.mjs +25 -3
  28. package/dist/assets/AgentProfilesPage-DQ8URegq.js +0 -1
  29. package/dist/assets/ChatPanelHost-vi1tDqlJ.js +0 -291
  30. package/dist/assets/ScheduledTasksPage-AaUz5el2.js +0 -2
  31. package/dist/assets/SharedConversationPage-9A4L9UcQ.js +0 -1
  32. package/dist/assets/TerminalDock-DQOXpaDe.js +0 -2
  33. package/dist/assets/WorkspaceInspector-DaM7TJHb.js +0 -13
  34. package/dist/assets/icons-ko_i0WpN.js +0 -1
  35. package/dist/assets/index-B2eauKpo.css +0 -3
  36. package/dist/assets/index-B8Awq5FV.js +0 -63
  37. package/dist/assets/mcp-servers-dialog-XE4K8PCO.js +0 -5
@@ -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,14 +299,76 @@ 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()
316
+ }
317
+ }
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
+ }
304
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))
305
372
  }
306
373
 
307
374
  export async function listGitStatus(context) {
@@ -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
 
@@ -662,53 +732,105 @@ async function handleWorkspaceFile(req, res, url) {
662
732
  })
663
733
  }
664
734
 
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
735
+ function createWorkspacePreviewError(message, statusCode, previewCode) {
736
+ const error = new Error(message)
737
+ error.statusCode = statusCode
738
+ error.previewCode = previewCode
739
+ return error
740
+ }
741
+
742
+ export function workspacePreviewIssueFromError(error, requestedPath = '') {
743
+ let status = error?.statusCode || 500
744
+ let code = error?.previewCode || 'PREVIEW_SERVICE_FAILED'
745
+
746
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
747
+ status = 404
748
+ code = 'PREVIEW_FILE_NOT_FOUND'
749
+ } else if (error?.name === 'URIError') {
750
+ status = 400
751
+ code = 'PREVIEW_INVALID_PATH'
752
+ } else if (error?.code === 'EACCES' || error?.code === 'EPERM') {
753
+ status = 403
754
+ code = 'PREVIEW_PERMISSION_DENIED'
755
+ } else if (status === 403 && !error?.previewCode) {
756
+ code = 'PREVIEW_PERMISSION_DENIED'
757
+ } else if (status === 400 && !error?.previewCode) {
758
+ code = 'PREVIEW_INVALID_PATH'
673
759
  }
674
760
 
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
761
+ return {
762
+ status,
763
+ payload: {
764
+ error: error?.message || 'Internal server error',
765
+ code,
766
+ path: requestedPath,
767
+ },
681
768
  }
769
+ }
682
770
 
683
- const context = await projectContextFromId(projectId)
771
+ export async function inspectWorkspacePreviewFile(context, relativePath) {
684
772
  const file = resolveWorkspacePath(relativePath, context)
685
773
  await assertSafeWorkspacePath(file, context)
686
774
  const extension = path.extname(file).toLowerCase()
687
775
  if (!PREVIEW_ALLOWED_EXTENSIONS.has(extension)) {
688
- const error = new Error('Unsupported preview file type')
689
- error.statusCode = 415
690
- throw error
776
+ throw createWorkspacePreviewError('Unsupported preview file type', 415, 'PREVIEW_UNSUPPORTED_TYPE')
691
777
  }
778
+
692
779
  const stat = await fs.stat(file)
693
780
  if (!stat.isFile()) {
694
- const error = new Error('Path is not a file')
695
- error.statusCode = 400
696
- throw error
781
+ throw createWorkspacePreviewError('Path is not a file', 400, 'PREVIEW_INVALID_PATH')
697
782
  }
698
783
  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
784
+ throw createWorkspacePreviewError('File is too large to preview', 413, 'PREVIEW_FILE_TOO_LARGE')
702
785
  }
703
786
 
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)
787
+ return {
788
+ file,
789
+ stat,
790
+ contentType: previewContentType(file),
791
+ }
792
+ }
793
+
794
+ async function handleWorkspacePreview(req, res, url) {
795
+ let relativePath = ''
796
+ try {
797
+ const prefix = '/api/workspace/preview/'
798
+ const tail = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : ''
799
+ const slashIndex = tail.indexOf('/')
800
+ if (slashIndex <= 0) {
801
+ throw createWorkspacePreviewError('projectId and path are required', 400, 'PREVIEW_INVALID_PATH')
802
+ }
803
+
804
+ const projectId = decodeURIComponent(tail.slice(0, slashIndex))
805
+ relativePath = decodeURIComponent(tail.slice(slashIndex + 1))
806
+ if (!projectId || !relativePath) {
807
+ throw createWorkspacePreviewError('projectId and path are required', 400, 'PREVIEW_INVALID_PATH')
808
+ }
809
+
810
+ const context = await projectContextFromId(projectId)
811
+ const preview = await inspectWorkspacePreviewFile(context, relativePath)
812
+ if (url.searchParams.get('__quickforge_check') === '1') {
813
+ sendJson(res, 200, {
814
+ ok: true,
815
+ path: relativePath,
816
+ size: preview.stat.size,
817
+ contentType: preview.contentType,
818
+ })
819
+ return
820
+ }
821
+
822
+ res.writeHead(200, {
823
+ 'content-type': preview.contentType,
824
+ 'cache-control': 'no-store',
825
+ 'x-content-type-options': 'nosniff',
826
+ })
827
+ const buffer = await fs.readFile(preview.file)
828
+ res.end(buffer)
829
+ } catch (error) {
830
+ const issue = workspacePreviewIssueFromError(error, relativePath)
831
+ if (issue.status >= 500) logger.error('Workspace preview failed', { error: issue.payload.error, path: relativePath })
832
+ sendJson(res, issue.status, issue.payload)
833
+ }
712
834
  }
713
835
 
714
836
  async function handleWorkspaceResolvePath(req, res) {
@@ -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{Ot as t,Q as n,St as r,T as i,Tt as a,a as ee,c as o}from"./icons-ko_i0WpN.js";import{i as s,n as c}from"./react-vendor-2RKYr-A4.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-B8Awq5FV.js";var p=e(t(),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,t]=(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,n]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);t(e.agents),c(n.tools)}(0,p.useEffect)(()=>{let e=!1;async function n(){try{let[n,r]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);if(e)return;t(n.agents),c(r.tools)}catch(t){e||z(t instanceof Error?t.message:d(`requestFailed`))}}return n(),()=>{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 n=!e.enabledAsSubagent,r=e.enabledAsSubagent;t(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:n}:t)),V(null);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`PATCH`,body:JSON.stringify({enabledAsSubagent:n})})}catch(n){t(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:r}:t)),z(n instanceof Error?n.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)(n,{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};