@shawnstack/quickforge 1.4.0 → 1.5.0

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 (65) hide show
  1. package/README.md +12 -12
  2. package/bin/quickforge.mjs +9 -0
  3. package/dist/assets/AgentProfilesPage-DUmXUxjA.js +1 -0
  4. package/dist/assets/ChatPanelHost-Syx0SSLe.js +242 -0
  5. package/dist/assets/PluginsPage-kiBq0gOT.js +1 -0
  6. package/dist/assets/ScheduledTasksPage-Dw4-tgp9.js +2 -0
  7. package/dist/assets/SharedConversationPage-CaE9bNb9.js +1 -0
  8. package/dist/assets/TerminalDock-BYJcp8Ts.js +2 -0
  9. package/dist/assets/WorkspaceInspector-Bzmv8Cvi.js +3 -0
  10. package/dist/assets/WorkspaceReaderDialog-BJo_KEWi.js +1 -0
  11. package/dist/assets/diff-line-counts-BZoYp5ai.js +10 -0
  12. package/dist/assets/icons-47L5YLKz.js +1 -0
  13. package/dist/assets/index-CqfScETb.js +1200 -0
  14. package/dist/assets/index-DzkBgHZf.css +3 -0
  15. package/dist/assets/{monaco-DG4TcBMc.js → monaco-CGq6uVF1.js} +1 -1
  16. package/dist/assets/{react-vendor-CiCXOLb5.js → react-vendor-DunfCFfp.js} +1 -1
  17. package/dist/favicon.svg +16 -1
  18. package/dist/index.html +5 -5
  19. package/dist/manifest.webmanifest +30 -30
  20. package/package.json +3 -2
  21. package/server/acp/server.mjs +921 -0
  22. package/server/agent-manager.mjs +283 -45
  23. package/server/agent-profile-files.mjs +179 -0
  24. package/server/agent-profiles.mjs +59 -5
  25. package/server/approval-store.mjs +13 -1
  26. package/server/auto-compaction.mjs +111 -112
  27. package/server/channels/process-channel.mjs +278 -0
  28. package/server/channels/providers/wechat.mjs +271 -0
  29. package/server/channels/registry.mjs +58 -0
  30. package/server/context-usage.mjs +108 -0
  31. package/server/custom-commands.mjs +157 -28
  32. package/server/frontmatter.mjs +167 -0
  33. package/server/index.mjs +52 -3
  34. package/server/mcp/registry.mjs +40 -0
  35. package/server/project-config.mjs +43 -6
  36. package/server/routes/agent-profiles.mjs +6 -2
  37. package/server/routes/agent.mjs +13 -2
  38. package/server/routes/channels.mjs +145 -0
  39. package/server/routes/mcp.mjs +7 -1
  40. package/server/routes/models.mjs +68 -0
  41. package/server/routes/project.mjs +34 -4
  42. package/server/routes/scheduled-tasks.mjs +6 -5
  43. package/server/routes/shared-conversation.mjs +1 -1
  44. package/server/routes/storage.mjs +4 -2
  45. package/server/routes/system.mjs +27 -0
  46. package/server/routes/tools.mjs +17 -6
  47. package/server/routes/workspace.mjs +138 -0
  48. package/server/session-utils.mjs +10 -2
  49. package/server/storage.mjs +30 -2
  50. package/server/subagents.mjs +8 -6
  51. package/server/system-prompt.mjs +3 -2
  52. package/server/tools/definitions.mjs +19 -1
  53. package/server/tools/index.mjs +83 -0
  54. package/server/utils/package-update.mjs +156 -0
  55. package/dist/assets/AgentProfilesPage-C79teCgh.js +0 -1
  56. package/dist/assets/ChatPanelHost-BjdIshtX.js +0 -195
  57. package/dist/assets/PluginsPage-Dt7Iiddo.js +0 -1
  58. package/dist/assets/ScheduledTasksPage-C047y3p3.js +0 -2
  59. package/dist/assets/SharedConversationPage-8X8kfztQ.js +0 -1
  60. package/dist/assets/TerminalDock-CEuJNf0m.js +0 -2
  61. package/dist/assets/WorkspaceInspector-BIa5gLVs.js +0 -3
  62. package/dist/assets/WorkspaceReaderDialog-bTeERaGd.js +0 -6
  63. package/dist/assets/icons-Dsc5yL3l.js +0 -1
  64. package/dist/assets/index-CPAWYhzz.css +0 -3
  65. package/dist/assets/index-YTL26wyJ.js +0 -814
@@ -12,6 +12,8 @@ import {
12
12
  } from '../utils/workspace.mjs'
13
13
 
14
14
  const MAX_PREVIEW_BYTES = 1024 * 1024
15
+ const MAX_STATIC_PREVIEW_BYTES = 10 * 1024 * 1024
16
+ const PREVIEW_ALLOWED_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.ico', '.txt', '.md'])
15
17
  const MAX_TREE_NODES = 5000
16
18
  const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'dist-ssr', 'package-dist', 'package-offline', '.vite', 'coverage'])
17
19
 
@@ -34,6 +36,28 @@ function languageFromPath(filePath) {
34
36
  return extensionLanguageMap.get(extension) || 'plaintext'
35
37
  }
36
38
 
39
+ function previewContentType(filePath) {
40
+ const ext = path.extname(filePath).toLowerCase()
41
+ const map = {
42
+ '.html': 'text/html; charset=utf-8',
43
+ '.htm': 'text/html; charset=utf-8',
44
+ '.js': 'application/javascript; charset=utf-8',
45
+ '.mjs': 'application/javascript; charset=utf-8',
46
+ '.css': 'text/css; charset=utf-8',
47
+ '.json': 'application/json; charset=utf-8',
48
+ '.svg': 'image/svg+xml',
49
+ '.png': 'image/png',
50
+ '.jpg': 'image/jpeg',
51
+ '.jpeg': 'image/jpeg',
52
+ '.webp': 'image/webp',
53
+ '.gif': 'image/gif',
54
+ '.ico': 'image/x-icon',
55
+ '.txt': 'text/plain; charset=utf-8',
56
+ '.md': 'text/markdown; charset=utf-8',
57
+ }
58
+ return map[ext] || 'application/octet-stream'
59
+ }
60
+
37
61
  function isBinaryBuffer(buffer) {
38
62
  const length = Math.min(buffer.length, 8000)
39
63
  for (let index = 0; index < length; index += 1) {
@@ -144,10 +168,71 @@ function countGitStatus(files) {
144
168
  }, { staged: 0, unstaged: 0, untracked: 0, conflicts: 0, total: 0 })
145
169
  }
146
170
 
171
+ function countTextLines(text) {
172
+ if (text.length === 0) return 0
173
+ return text.split('\n').length - (text.endsWith('\n') ? 1 : 0)
174
+ }
175
+
176
+ // numstat 的路径列对 rename 用 "prefix/{old => new}" 或 "old => new" 形式,取新路径
177
+ function numstatNewPath(rawPath) {
178
+ const arrow = rawPath.indexOf(' => ')
179
+ if (arrow < 0) return rawPath
180
+ const head = rawPath.slice(0, arrow)
181
+ const tail = rawPath.slice(arrow + 4)
182
+ const brace = head.lastIndexOf('{')
183
+ if (brace < 0) return tail
184
+ return `${head.slice(0, brace)}${tail.replace(/\}$/, '')}`
185
+ }
186
+
187
+ // 工作区 vs HEAD 的每个文件增删行数(口径与 git diff --numstat 一致)
188
+ async function collectNumstat(context) {
189
+ const map = new Map()
190
+ const result = await git(['diff', 'HEAD', '--numstat', '-z'], context.workspaceRoot, { allowFailure: true })
191
+ if (result.code !== 0) return map
192
+ const records = result.stdout.toString('utf8').split('\0').filter(Boolean)
193
+ for (const record of records) {
194
+ const fields = record.split('\t')
195
+ if (fields.length < 3) continue
196
+ const added = fields[0]
197
+ const removed = fields[1]
198
+ const rawPath = fields.slice(2).join('\t')
199
+ if (added === '-' || removed === '-') continue // 二进制文件
200
+ const additions = Number(added)
201
+ const deletions = Number(removed)
202
+ if (!Number.isFinite(additions) || !Number.isFinite(deletions)) continue
203
+ map.set(numstatNewPath(rawPath), { additions, deletions })
204
+ }
205
+ return map
206
+ }
207
+
208
+ // 未跟踪文件不在 numstat 中,按工作区文件行数估算新增行
209
+ async function countWorkspaceLines(context, relativePath) {
210
+ try {
211
+ const { content } = await readWorkspaceTextFile(context, relativePath)
212
+ return countTextLines(content)
213
+ } catch {
214
+ return undefined
215
+ }
216
+ }
217
+
147
218
  async function listGitStatus(context) {
148
219
  if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
149
220
  const result = await git(['status', '--porcelain=v1', '-z'], context.workspaceRoot)
150
221
  const files = parseGitStatus(result.stdout)
222
+ const numstat = await collectNumstat(context)
223
+ for (const file of files) {
224
+ const entry = numstat.get(file.path)
225
+ if (entry) {
226
+ file.additions = entry.additions
227
+ file.deletions = entry.deletions
228
+ } else if (file.status === 'untracked' || file.status === 'added') {
229
+ const count = await countWorkspaceLines(context, file.path)
230
+ if (typeof count === 'number') {
231
+ file.additions = count
232
+ file.deletions = 0
233
+ }
234
+ }
235
+ }
151
236
  return {
152
237
  isGitRepository: true,
153
238
  branch: await currentGitBranch(context.workspaceRoot),
@@ -246,6 +331,55 @@ async function handleWorkspaceFile(req, res, url) {
246
331
  })
247
332
  }
248
333
 
334
+ async function handleWorkspacePreview(req, res, url) {
335
+ const prefix = '/api/workspace/preview/'
336
+ const tail = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : ''
337
+ const slashIndex = tail.indexOf('/')
338
+ if (slashIndex <= 0) {
339
+ const error = new Error('projectId and path are required')
340
+ error.statusCode = 400
341
+ throw error
342
+ }
343
+
344
+ const projectId = decodeURIComponent(tail.slice(0, slashIndex))
345
+ const relativePath = decodeURIComponent(tail.slice(slashIndex + 1))
346
+ if (!projectId || !relativePath) {
347
+ const error = new Error('projectId and path are required')
348
+ error.statusCode = 400
349
+ throw error
350
+ }
351
+
352
+ const context = await projectContextFromId(projectId)
353
+ const file = resolveWorkspacePath(relativePath, context)
354
+ await assertSafeWorkspacePath(file, context)
355
+ const extension = path.extname(file).toLowerCase()
356
+ if (!PREVIEW_ALLOWED_EXTENSIONS.has(extension)) {
357
+ const error = new Error('Unsupported preview file type')
358
+ error.statusCode = 415
359
+ throw error
360
+ }
361
+ const stat = await fs.stat(file)
362
+ if (!stat.isFile()) {
363
+ const error = new Error('Path is not a file')
364
+ error.statusCode = 400
365
+ throw error
366
+ }
367
+ if (stat.size > MAX_STATIC_PREVIEW_BYTES) {
368
+ const error = new Error('File is too large to preview')
369
+ error.statusCode = 413
370
+ throw error
371
+ }
372
+
373
+ const contentType = previewContentType(file)
374
+ res.writeHead(200, {
375
+ 'content-type': contentType,
376
+ 'cache-control': 'no-store',
377
+ 'x-content-type-options': 'nosniff',
378
+ })
379
+ const buffer = await fs.readFile(file)
380
+ res.end(buffer)
381
+ }
382
+
249
383
  async function handleWorkspaceResolvePath(req, res) {
250
384
  const body = await readJsonBody(req, 16 * 1024)
251
385
  const projectId = typeof body?.projectId === 'string' ? body.projectId : ''
@@ -344,6 +478,10 @@ export async function handleWorkspaceApi(req, res, url) {
344
478
  await handleWorkspaceFile(req, res, url)
345
479
  return
346
480
  }
481
+ if (req.method === 'GET' && url.pathname.startsWith('/api/workspace/preview/')) {
482
+ await handleWorkspacePreview(req, res, url)
483
+ return
484
+ }
347
485
  if (req.method === 'POST' && url.pathname === '/api/workspace/resolve-path') {
348
486
  await handleWorkspaceResolvePath(req, res)
349
487
  return
@@ -1,5 +1,5 @@
1
1
  import { streamSimple } from '@earendil-works/pi-ai'
2
- import { buildInstructionsPayload } from './project-config.mjs'
2
+ import { buildInstructionsPayload, projectContextFromId } from './project-config.mjs'
3
3
  import { composeSystemPrompt } from './system-prompt.mjs'
4
4
  import { listSubagentProfiles } from './agent-profiles.mjs'
5
5
 
@@ -9,9 +9,17 @@ import { listSubagentProfiles } from './agent-profiles.mjs'
9
9
 
10
10
  export async function buildSystemPrompt(projectId) {
11
11
  const instructions = await buildInstructionsPayload(projectId)
12
+ let workspaceRoot = instructions.workspace?.root || null
13
+ if (projectId && !workspaceRoot) {
14
+ try {
15
+ workspaceRoot = (await projectContextFromId(projectId))?.workspaceRoot || null
16
+ } catch {
17
+ // project may have been removed; fall back to global agent profile discovery
18
+ }
19
+ }
12
20
  return composeSystemPrompt({
13
21
  ...instructions,
14
- subagents: await listSubagentProfiles(),
22
+ subagents: await listSubagentProfiles({ projectId, workspaceRoot }),
15
23
  })
16
24
  }
17
25
 
@@ -94,6 +94,7 @@ export const configDir = path.join(dataDir, 'config')
94
94
  export const storageDir = path.join(dataDir, 'storage')
95
95
  export const cacheDir = path.join(dataDir, 'cache')
96
96
  export const logsDir = path.join(dataDir, 'logs')
97
+ export const userCommandsDir = path.join(dataDir, 'commands')
97
98
 
98
99
  const quickForgeConfigFile = path.join(configDir, 'config.json')
99
100
  const configMigrationMarkerFile = path.join(configDir, '.layout-migrated')
@@ -467,6 +468,26 @@ async function rebuildBucketIndex() {
467
468
  bucketIndexBuilt = true
468
469
  }
469
470
 
471
+ async function findSessionBucketByDataFile(sessionId) {
472
+ assertSafePathSegment(sessionId)
473
+ const buckets = [
474
+ { scope: 'global' },
475
+ ...(await listProjectIds()).map((projectId) => ({ scope: 'project', projectId })),
476
+ ]
477
+
478
+ for (const bucket of buckets) {
479
+ const file = sessionDataFile(sessionId, bucket)
480
+ try {
481
+ const stat = await fs.stat(file)
482
+ if (stat.isFile()) return bucket
483
+ } catch (error) {
484
+ if (error?.code !== 'ENOENT') throw error
485
+ }
486
+ }
487
+
488
+ return null
489
+ }
490
+
470
491
  export async function findSessionBucket(sessionId) {
471
492
  if (!bucketIndexBuilt) {
472
493
  await ensureStorage()
@@ -477,8 +498,12 @@ export async function findSessionBucket(sessionId) {
477
498
 
478
499
  export async function readSessionValue(sessionId) {
479
500
  const bucket = await findSessionBucket(sessionId)
480
- if (!bucket) return null
481
- return readJsonFile(sessionDataFile(sessionId, bucket), null)
501
+ if (bucket) return readJsonFile(sessionDataFile(sessionId, bucket), null)
502
+
503
+ const recoveredBucket = await findSessionBucketByDataFile(sessionId)
504
+ if (!recoveredBucket) return null
505
+ sessionBucketIndex.set(sessionId, recoveredBucket)
506
+ return readJsonFile(sessionDataFile(sessionId, recoveredBucket), null)
482
507
  }
483
508
 
484
509
  export async function writeSessionValue(sessionId, value) {
@@ -724,6 +749,9 @@ export function ensureStorage() {
724
749
  fs.mkdir(path.join(cacheDir, 'projects'), { recursive: true }),
725
750
  fs.mkdir(path.join(storageDir, 'conversations', 'global', 'sessions'), { recursive: true }),
726
751
  fs.mkdir(path.join(storageDir, 'conversations', 'projects'), { recursive: true }),
752
+ // Default workspace directory for global (non-project) conversations, so
753
+ // they share the same file-tool capabilities as project conversations.
754
+ fs.mkdir(path.join(dataDir, 'workspace'), { recursive: true }),
727
755
  cleanOldLogs(),
728
756
  ])
729
757
 
@@ -21,23 +21,23 @@ export const subagentDefinitions = [
21
21
  name: 'general',
22
22
  label: 'General',
23
23
  mode: 'subagent',
24
- description: 'A general-purpose agent for researching complex problems and executing multi-step tasks. It has full built-in workspace tool access, excluding MCP tools and Agent Skills, so it can modify files when needed. Use it for substantial independent work units, including parallelizable work.',
24
+ description: 'A general-purpose agent for bounded complex multi-step implementation or broader independent work. It has full built-in workspace tool access, excluding MCP tools and Agent Skills, so it can modify files when needed. Prefer Explore for focused read-only repository discovery, source search, call-chain lookup, tests/docs discovery, and impact analysis.',
25
25
  allowedTools: ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command'],
26
26
  allowFileMutations: true,
27
27
  maxRuntimeMs: 30 * 60 * 1000,
28
28
  maxToolCalls: 300,
29
- systemPrompt: `You are General, a general-purpose subagent for complex research and multi-step implementation tasks. You may inspect, edit, write files, and run commands using the built-in workspace tools when needed. You do not have MCP tools or Agent Skills. Make focused, minimal changes that satisfy the delegated task, and verify your changes when appropriate.`,
29
+ systemPrompt: `You are General, a general-purpose subagent for bounded complex multi-step implementation tasks and broader independent work. You may inspect, edit, write files, and run commands using the built-in workspace tools when needed. You do not have MCP tools or Agent Skills. Prefer Explore for focused read-only repository discovery, source search, call-chain lookup, tests/docs discovery, and impact analysis. Make focused, minimal changes that satisfy the delegated task, and verify your changes when appropriate.`,
30
30
  },
31
31
  {
32
32
  name: 'explore',
33
33
  label: 'Explore',
34
34
  mode: 'subagent',
35
- description: 'A fast read-only agent for targeted exploration and focused questions. It cannot modify files. Use it to quickly find relevant files, search keywords, identify patterns, or summarize findings.',
36
- allowedTools: ['read_file', 'grep_files'],
35
+ description: 'The preferred subagent for focused read-only repository exploration, file discovery, source search, call-chain lookup, related tests/docs/wiki discovery, safe inspection commands, pattern lookup, and impact analysis before non-trivial implementation. It cannot modify files.',
36
+ allowedTools: ['read_file', 'grep_files', 'run_command'],
37
37
  allowFileMutations: false,
38
38
  maxRuntimeMs: 30 * 60 * 1000,
39
39
  maxToolCalls: 300,
40
- systemPrompt: `You are Explore, a fast read-only exploration subagent. Use read_file and grep_files to locate relevant files, search keywords, identify patterns, and answer focused questions. You cannot modify files or run commands.`,
40
+ systemPrompt: `You are Explore, the preferred read-only repository exploration subagent. Use read_file, grep_files, and safe read-only run_command calls to locate files, inspect project structure, search source, trace call chains, find related tests/docs/wiki pages, run diagnostics, identify patterns, assess impact, and answer focused questions before non-trivial implementation. You cannot modify files.`,
41
41
  },
42
42
  ]
43
43
 
@@ -80,7 +80,9 @@ export function composeSubagentSystemPrompt({ definition, parentSystemPrompt, pr
80
80
  '- run_subagent is not available to subagents.',
81
81
  definition.allowFileMutations
82
82
  ? '- File modification tools are available when needed, subject to the parent session approval/YOLO policy.'
83
- : '- This subagent is read-only. Do not modify files or run commands.',
83
+ : definition.allowedTools.includes('run_command')
84
+ ? '- This subagent is read-only. Do not modify files. Use run_command only for safe inspection or diagnostic commands.'
85
+ : '- This subagent is read-only. Do not modify files or run commands.',
84
86
  workspaceLines.length ? `\nWorkspace context:\n${workspaceLines.join('\n')}` : '',
85
87
  '</subagent_instructions>',
86
88
  ].filter(Boolean).join('\n')
@@ -5,10 +5,11 @@ For project tasks:
5
5
  - Prefer the simplest solution that satisfies the request.
6
6
  - Make surgical changes only. Do not refactor unrelated code.
7
7
  - Match existing style.
8
+ - When content has room for visual explanation, first consider whether an SVG diagram can improve understanding.
8
9
  - For multi-step work, use a brief plan.
9
10
  - Before changing files, gather sufficient context: relevant files, entry points or call chains, existing patterns, tests or validation commands, and docs/wiki impact.
10
11
  - Before taking action, confirm with the user.
11
- - Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research; prefer Explore for broad searches, pattern lookup, impact analysis, and locating related tests, docs, or build scripts.
12
+ - Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research before implementation decisions; prefer Explore for file discovery, source location, broad searches, call-chain lookup, pattern lookup, impact analysis, and locating related tests, docs, wiki pages, or build scripts.
12
13
  - For complex multi-step work, use General only for bounded assistance; the parent assistant remains responsible for final decisions, minimal edits, and verification.
13
14
  - Make minimal, focused changes.
14
15
  - Prefer dedicated workspace tools for reading, editing, and searching files.
@@ -63,7 +64,7 @@ function appendSubagentCatalog(parts, subagents) {
63
64
  const subagentParts = subagents.map(formatSubagentCatalogItem)
64
65
  parts.push(`
65
66
  <available_subagents>
66
- The run_subagent tool can delegate work to an enabled temporary Agent Profile. Built-in profiles include Explore for fast read-only codebase search and General for complex multi-step implementation work; custom profiles may also be available when enabled.
67
+ The run_subagent tool can delegate work to an enabled temporary Agent Profile. Prefer Explore for focused read-only repository discovery before implementation decisions, including locating files, searching source, tracing call chains, finding related tests/docs/wiki pages, and impact analysis. Use General for bounded complex multi-step implementation or broader independent work; custom profiles may also be available when enabled.
67
68
 
68
69
  Choose the most appropriate subagent by name, keep delegation concrete, and include relevant context. Treat subagent output as advisory; you remain responsible for the final answer.
69
70
 
@@ -15,7 +15,7 @@ import { loadSelectedGlobalSkills, loadSelectedProjectSkills, mergeSkills } from
15
15
  export const subagentTool = {
16
16
  name: 'run_subagent',
17
17
  label: 'Run subagent',
18
- description: 'Delegate a bounded task to an enabled temporary Agent Profile. Built-in profiles include general for complex multi-step work and explore for fast read-only lookup. Custom profiles can also be enabled as subagents. Subagents are short-lived and do not receive MCP or Agent Skill tools.',
18
+ description: 'Delegate a bounded task to an enabled temporary Agent Profile. Prefer explore for focused read-only repository discovery before implementation decisions, including locating files, searching source, tracing call chains, finding related tests/docs/wiki pages, and impact analysis. Use general for bounded complex multi-step implementation or broader independent work. Custom profiles can also be enabled as subagents. Subagents are short-lived and do not receive MCP or Agent Skill tools.',
19
19
  parameters: Type.Object({
20
20
  subagent: Type.String({ description: 'Agent Profile name to invoke.' }),
21
21
  task: Type.String({ description: 'Concrete, bounded task for the subagent. Do not delegate vague or open-ended work.' }),
@@ -86,6 +86,24 @@ export const workspaceTools = [
86
86
  }),
87
87
  executionMode: 'sequential',
88
88
  },
89
+ {
90
+ name: 'present_files',
91
+ label: 'Present files',
92
+ description: 'Show one or more AI-produced artifact files to the user. Use this after creating or editing user-facing files such as HTML pages, SVG/images, Markdown documents, or other deliverables. HTML files will be previewed directly in the artifact preview panel.',
93
+ parameters: Type.Object({
94
+ files: Type.Array(Type.Union([
95
+ Type.String({ description: 'File path relative to the workspace root.' }),
96
+ Type.Object({
97
+ path: Type.String({ description: 'File path relative to the workspace root.' }),
98
+ title: Type.Optional(Type.String({ description: 'Optional display title.' })),
99
+ description: Type.Optional(Type.String({ description: 'Optional short description shown in artifact lists.' })),
100
+ kind: Type.Optional(Type.String({ description: 'Optional file kind hint, such as html, image, markdown, or code.' })),
101
+ preview: Type.Optional(Type.Boolean({ description: 'Whether this file should be opened as the default preview.' })),
102
+ }),
103
+ ]), { description: 'Artifact files to present.' }),
104
+ defaultPreview: Type.Optional(Type.String({ description: 'File path to open as the default preview when multiple files are presented.' })),
105
+ }),
106
+ },
89
107
  ]
90
108
 
91
109
  function activeSkillSchema(skills) {
@@ -619,6 +619,88 @@ export async function toolReadSkillResource(params, context) {
619
619
  }
620
620
  }
621
621
 
622
+ // --- present_files ---
623
+ function inferPresentedFileKind(relativePath) {
624
+ const lower = String(relativePath || '').toLowerCase()
625
+ if (lower.endsWith('.html') || lower.endsWith('.htm')) return 'html'
626
+ if (/\.(svg|png|jpe?g|webp|gif|bmp|ico)$/.test(lower)) return 'image'
627
+ if (lower.endsWith('.md') || lower.endsWith('.mdx')) return 'markdown'
628
+ if (/\.(ts|tsx|js|jsx|mjs|cjs|css|scss|less|json|jsonc|txt|xml|yml|yaml|toml|ini|py|rb|go|rs|java|c|h|cpp|hpp|cs|php|sh|bash|zsh|ps1)$/.test(lower)) return 'code'
629
+ return 'unknown'
630
+ }
631
+
632
+ function normalizePresentFileEntry(entry) {
633
+ if (typeof entry === 'string') return { path: entry }
634
+ if (entry && typeof entry === 'object' && typeof entry.path === 'string') {
635
+ return {
636
+ path: entry.path,
637
+ title: typeof entry.title === 'string' ? entry.title : undefined,
638
+ description: typeof entry.description === 'string' ? entry.description : undefined,
639
+ kind: typeof entry.kind === 'string' ? entry.kind : undefined,
640
+ preview: typeof entry.preview === 'boolean' ? entry.preview : undefined,
641
+ }
642
+ }
643
+ return undefined
644
+ }
645
+
646
+ export async function toolPresentFiles(params, context) {
647
+ const rawFiles = Array.isArray(params?.files) ? params.files : []
648
+ const defaultPreviewInput = typeof params?.defaultPreview === 'string' ? params.defaultPreview : undefined
649
+ if (rawFiles.length === 0) {
650
+ const error = new Error('files must contain at least one path')
651
+ error.statusCode = 400
652
+ throw error
653
+ }
654
+
655
+ const files = []
656
+ const seen = new Set()
657
+ for (const raw of rawFiles) {
658
+ const entry = normalizePresentFileEntry(raw)
659
+ if (!entry?.path?.trim()) continue
660
+ const file = resolveWorkspacePath(entry.path, context)
661
+ await assertSafeWorkspacePath(file, context)
662
+ const stat = await fs.stat(file)
663
+ if (!stat.isFile()) {
664
+ const error = new Error(`Path is not a file: ${entry.path}`)
665
+ error.statusCode = 400
666
+ throw error
667
+ }
668
+ const relativePath = toWorkspaceRelative(file, context)
669
+ const key = relativePath.toLowerCase()
670
+ if (seen.has(key)) continue
671
+ seen.add(key)
672
+ const kind = entry.kind || inferPresentedFileKind(relativePath)
673
+ const isDefaultPreview = defaultPreviewInput ? relativePath === defaultPreviewInput.replace(/\\/g, '/') : false
674
+ files.push({
675
+ path: relativePath,
676
+ title: entry.title,
677
+ description: entry.description,
678
+ kind,
679
+ preview: entry.preview ?? (isDefaultPreview || kind === 'html'),
680
+ defaultPreview: isDefaultPreview,
681
+ bytes: stat.size,
682
+ })
683
+ }
684
+
685
+ if (files.length === 0) {
686
+ const error = new Error('No valid files to present')
687
+ error.statusCode = 400
688
+ throw error
689
+ }
690
+
691
+ const previewed = files.filter((file) => file.preview || file.kind === 'html').map((file) => file.path)
692
+ return {
693
+ content: `Presented ${files.length} file(s)${previewed.length ? ` and opened ${previewed.length} preview(s)` : ''}: ${files.map((file) => file.path).join(', ')}`,
694
+ details: {
695
+ type: 'present_files_result',
696
+ files,
697
+ defaultPreview: files.find((file) => file.defaultPreview)?.path,
698
+ previewed,
699
+ project: context?.project,
700
+ },
701
+ }
702
+ }
703
+
622
704
  // --- run_command ---
623
705
  const DEFAULT_RUN_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
624
706
  const MIN_RUN_COMMAND_TIMEOUT_MS = 1000
@@ -1009,6 +1091,7 @@ export const toolHandlers = {
1009
1091
  write_file: toolWriteFile,
1010
1092
  edit_file: toolEditFile,
1011
1093
  run_command: toolRunCommand,
1094
+ present_files: toolPresentFiles,
1012
1095
  activate_skill: toolActivateSkill,
1013
1096
  read_skill_resource: toolReadSkillResource,
1014
1097
  }
@@ -0,0 +1,156 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { promises as fs } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ function normalizeRepositoryUrl(value) {
6
+ if (!value || typeof value !== 'string') return ''
7
+ return value
8
+ .replace(/^git\+/i, '')
9
+ .replace(/^git@github\.com:/i, 'https://github.com/')
10
+ .replace(/\.git$/i, '')
11
+ }
12
+
13
+ export async function getPackageInfo(projectRoot) {
14
+ const packageJsonPath = path.join(projectRoot, 'package.json')
15
+ try {
16
+ const text = await fs.readFile(packageJsonPath, 'utf8')
17
+ const pkg = JSON.parse(text)
18
+ const repositoryUrl = normalizeRepositoryUrl(
19
+ typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url,
20
+ )
21
+
22
+ return {
23
+ name: pkg.name || 'quickforge',
24
+ version: pkg.version || '0.0.0',
25
+ repositoryUrl,
26
+ homepage: pkg.homepage || repositoryUrl,
27
+ bugsUrl: typeof pkg.bugs === 'string' ? pkg.bugs : pkg.bugs?.url || '',
28
+ }
29
+ } catch (error) {
30
+ throw new Error(`Unable to read package metadata: ${error.message}`)
31
+ }
32
+ }
33
+
34
+ function normalizeVersion(version) {
35
+ return String(version || '').trim().replace(/^v/i, '')
36
+ }
37
+
38
+ function parseVersion(version) {
39
+ const [main, prerelease = ''] = normalizeVersion(version).split('-', 2)
40
+ const numbers = main.split('.').slice(0, 3).map((part) => {
41
+ const value = Number(part)
42
+ return Number.isFinite(value) ? value : 0
43
+ })
44
+
45
+ while (numbers.length < 3) numbers.push(0)
46
+ return { numbers, prerelease }
47
+ }
48
+
49
+ function comparePrerelease(left, right) {
50
+ if (left === right) return 0
51
+ if (!left) return 1
52
+ if (!right) return -1
53
+
54
+ const leftParts = left.split('.')
55
+ const rightParts = right.split('.')
56
+ const maxLength = Math.max(leftParts.length, rightParts.length)
57
+
58
+ for (let i = 0; i < maxLength; i += 1) {
59
+ const leftPart = leftParts[i]
60
+ const rightPart = rightParts[i]
61
+ if (leftPart === rightPart) continue
62
+ if (leftPart === undefined) return -1
63
+ if (rightPart === undefined) return 1
64
+
65
+ const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null
66
+ const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null
67
+
68
+ if (leftNumber !== null && rightNumber !== null) return leftNumber > rightNumber ? 1 : -1
69
+ if (leftNumber !== null) return -1
70
+ if (rightNumber !== null) return 1
71
+
72
+ return leftPart > rightPart ? 1 : -1
73
+ }
74
+
75
+ return 0
76
+ }
77
+
78
+ export function compareVersions(left, right) {
79
+ const parsedLeft = parseVersion(left)
80
+ const parsedRight = parseVersion(right)
81
+
82
+ for (let i = 0; i < 3; i += 1) {
83
+ if (parsedLeft.numbers[i] > parsedRight.numbers[i]) return 1
84
+ if (parsedLeft.numbers[i] < parsedRight.numbers[i]) return -1
85
+ }
86
+
87
+ return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease)
88
+ }
89
+
90
+ function getRegistryPackageUrl(packageName) {
91
+ const registry = (process.env.npm_config_registry || 'https://registry.npmjs.org/').replace(/\/+$/, '')
92
+ return `${registry}/${encodeURIComponent(packageName)}`
93
+ }
94
+
95
+ export async function fetchLatestVersion(packageName) {
96
+ const controller = new AbortController()
97
+ const timeout = setTimeout(() => controller.abort(), 5000)
98
+
99
+ try {
100
+ const response = await fetch(getRegistryPackageUrl(packageName), {
101
+ headers: { accept: 'application/json' },
102
+ signal: controller.signal,
103
+ })
104
+
105
+ if (!response.ok) throw new Error(`registry returned HTTP ${response.status}`)
106
+
107
+ const metadata = await response.json()
108
+ const latest = metadata?.['dist-tags']?.latest
109
+ if (!latest || typeof latest !== 'string') throw new Error('latest version not found in registry response')
110
+ return latest
111
+ } catch (error) {
112
+ if (error.name === 'AbortError') throw new Error('request timeout')
113
+ throw error
114
+ } finally {
115
+ clearTimeout(timeout)
116
+ }
117
+ }
118
+
119
+ export async function checkForUpdates(projectRoot) {
120
+ const pkg = await getPackageInfo(projectRoot)
121
+ const latestVersion = await fetchLatestVersion(pkg.name)
122
+ const comparison = compareVersions(pkg.version, latestVersion)
123
+ return {
124
+ ...pkg,
125
+ currentVersion: pkg.version,
126
+ latestVersion,
127
+ updateAvailable: comparison < 0,
128
+ localVersionIsNewer: comparison > 0,
129
+ installCommand: `npm install -g ${pkg.name}@latest`,
130
+ }
131
+ }
132
+
133
+ function getNpmCommand() {
134
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm'
135
+ }
136
+
137
+ export async function installLatestVersion(packageName, options = {}) {
138
+ const target = `${packageName}@latest`
139
+ const child = spawn(getNpmCommand(), ['install', '-g', target], {
140
+ cwd: options.cwd,
141
+ stdio: 'ignore',
142
+ shell: process.platform === 'win32',
143
+ windowsHide: true,
144
+ })
145
+
146
+ return new Promise((resolve, reject) => {
147
+ child.on('error', reject)
148
+ child.on('exit', (code) => {
149
+ if (code === 0) {
150
+ resolve()
151
+ return
152
+ }
153
+ reject(new Error(`npm install exited with code ${code}`))
154
+ })
155
+ })
156
+ }
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{it as t,nt as n,s as r}from"./icons-Dsc5yL3l.js";import{n as i}from"./react-vendor-CiCXOLb5.js";import{E as a,O as o,S as s,T as c,b as l,v as u,x as d,y as f}from"./index-YTL26wyJ.js";var p=e(t(),1),m=i();function h(){return{name:``,label:``,description:``,systemPrompt:``,allowedTools:[`read_file`,`grep_files`],maxRuntimeMs:`1800000`,maxToolCalls:`300`,enabledAsSubagent:!0}}function g(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}}function _(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}}function v(e){return!!(e.name.trim()&&e.label.trim()&&e.allowedTools.length>0)}async function y(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 b(){let[e,t]=(0,p.useState)([]),[i,b]=(0,p.useState)([]),[x,S]=(0,p.useState)(!1),[C,w]=(0,p.useState)(null),[T,E]=(0,p.useState)(()=>h()),[D,O]=(0,p.useState)(!1),[k,A]=(0,p.useState)(``),[j,M]=(0,p.useState)(!1),[N,P]=(0,p.useState)(),[F,I]=(0,p.useState)(`off`),[L,R]=(0,p.useState)(``);async function z(){let[e,n]=await Promise.all([y(`/api/agent-profiles`),y(`/api/agent-profiles/available-tools`)]);t(e.agents),b(n.tools)}(0,p.useEffect)(()=>{let e=!1;async function n(){try{let[n,r]=await Promise.all([y(`/api/agent-profiles`),y(`/api/agent-profiles/available-tools`)]);if(e)return;t(n.agents),b(r.tools)}catch(t){e||R(t instanceof Error?t.message:o(`requestFailed`))}}return n(),()=>{e=!0}},[]),(0,p.useEffect)(()=>{let e=!1;async function t(){try{let t=await l(),n=await f(t),r=await d(t),i=r.model??await s(t)??n[0];if(e)return;P(i),I(r.thinkingLevel??u(i))}catch{}}return t(),()=>{e=!0}},[]);let B=(0,p.useMemo)(()=>e.find(e=>e.id===C)??null,[e,C]);function V(e,t){E(n=>({...n,[e]:t}))}function H(e){E(t=>({...t,allowedTools:t.allowedTools.includes(e)?t.allowedTools.filter(t=>t!==e):[...t.allowedTools,e]}))}function U(){w(null),E(h()),A(``),R(``),S(!0)}function W(e){w(e.id),E(g(e)),A(``),R(``),S(!0)}function G(){D||j||(S(!1),w(null),E(h()),A(``))}async function K(){let e=k.trim();if(!e){R(o(`aiFillAgentInputRequired`));return}if(!N){R(o(`aiFillAgentNoModel`));return}M(!0),R(``);try{let t=await y(`/api/agent-profiles/ai-fill`,{method:`POST`,body:JSON.stringify({instruction:e,model:N,thinkingLevel:F})});E(e=>({...e,name:t.agent.name,label:t.agent.label,description:t.agent.description,systemPrompt:t.agent.systemPrompt}))}catch(e){R(e instanceof Error?e.message:o(`aiFillAgentFailed`))}finally{M(!1)}}async function q(){if(v(T)){O(!0),R(``);try{let e=_(T);C?await y(`/api/agent-profiles/${encodeURIComponent(C)}`,{method:`PATCH`,body:JSON.stringify(e)}):await y(`/api/agent-profiles`,{method:`POST`,body:JSON.stringify(e)}),G(),await z()}catch(e){R(e instanceof Error?e.message:o(`requestFailed`))}finally{O(!1)}}}async function J(e){if(!e.builtin&&await c({description:o(`confirmDeleteAgent`),confirmLabel:o(`confirmDelete`),cancelLabel:o(`cancel`),variant:`destructive`})){R(``);try{await y(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await z()}catch(e){R(e instanceof Error?e.message:o(`requestFailed`))}}}return(0,m.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col overflow-hidden bg-background`,children:[(0,m.jsx)(`div`,{className:`border-b border-border px-6 py-5`,children:(0,m.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[(0,m.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,m.jsx)(`div`,{className:`flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary`,children:(0,m.jsx)(n,{className:`size-5`})}),(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`h1`,{className:`text-lg font-semibold text-foreground`,children:o(`agentsTab`)}),(0,m.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:o(`agentsDescription`)})]})]}),(0,m.jsx)(a,{onClick:U,children:o(`createAgent`)})]})}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto p-6`,children:(0,m.jsxs)(`div`,{className:`mx-auto max-w-5xl space-y-5`,children:[L&&!x?(0,m.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:L}):null,(0,m.jsx)(`div`,{className:`grid gap-4 md:grid-cols-2`,children:e.map(e=>(0,m.jsxs)(`div`,{className:`rounded-2xl border border-border bg-card p-4 shadow-sm`,children:[(0,m.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,m.jsxs)(`div`,{className:`min-w-0`,children:[(0,m.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,m.jsx)(`h3`,{className:`truncate text-base font-semibold text-foreground`,children:e.label}),e.builtin?(0,m.jsx)(`span`,{className:`rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary`,children:o(`builtinAgent`)}):null,e.enabledAsSubagent?(0,m.jsx)(`span`,{className:`rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs text-emerald-700`,children:o(`enabledAsSubagent`)}):null]}),(0,m.jsx)(`p`,{className:`mt-1 font-mono text-xs text-muted-foreground`,children:e.name}),(0,m.jsx)(`p`,{className:`mt-2 text-sm text-muted-foreground`,children:e.description||o(`noDescription`)})]}),(0,m.jsxs)(`div`,{className:`flex shrink-0 gap-1`,children:[(0,m.jsx)(a,{variant:`outline`,size:`sm`,disabled:e.builtin,onClick:()=>W(e),children:o(`editTask`)}),(0,m.jsx)(a,{variant:`destructive`,size:`sm`,disabled:e.builtin,onClick:()=>void J(e),children:o(`delete`)})]})]}),(0,m.jsx)(`div`,{className:`mt-3 flex flex-wrap gap-1`,children:e.allowedTools.map(e=>(0,m.jsx)(`span`,{className:`rounded-full bg-muted px-2 py-0.5 font-mono text-xs text-muted-foreground`,children:e},e))}),(0,m.jsxs)(`div`,{className:`mt-3 grid gap-2 border-t border-border pt-3 text-xs text-muted-foreground sm:grid-cols-2`,children:[(0,m.jsxs)(`span`,{children:[o(`maxRuntimeMs`),e.maxRuntimeMs??`-`]}),(0,m.jsxs)(`span`,{children:[o(`maxToolCalls`),e.maxToolCalls??`-`]})]})]},e.id))})]})}),x?(0,m.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onMouseDown:e=>{e.target===e.currentTarget&&G()},children:(0,m.jsxs)(`div`,{className:`flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-2xl`,onMouseDown:e=>e.stopPropagation(),children:[(0,m.jsxs)(`div`,{className:`shrink-0 border-b border-border px-5 py-4`,children:[(0,m.jsx)(`h2`,{className:`text-base font-semibold text-foreground`,children:o(B?`editAgent`:`createAgent`)}),B?.builtin?(0,m.jsx)(`p`,{className:`mt-1 text-sm text-muted-foreground`,children:o(`builtinAgentReadonly`)}):null]}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto 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)(r,{className:`size-4 text-primary`}),o(`aiFillAgent`)]}),(0,m.jsx)(`p`,{className:`mb-2 text-xs text-muted-foreground`,children:o(`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:k,disabled:!!B?.builtin||j,onChange:e=>A(e.target.value),placeholder:o(`aiFillAgentPlaceholder`)}),(0,m.jsx)(`div`,{className:`mt-2 flex justify-end`,children:(0,m.jsxs)(a,{variant:`outline`,size:`sm`,onClick:()=>void K(),disabled:!!B?.builtin||j||!k.trim(),children:[(0,m.jsx)(r,{className:`mr-1 size-3.5`}),o(j?`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:[o(`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:T.name,disabled:!!B?.builtin,onChange:e=>V(`name`,e.target.value),placeholder:`reviewer`})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`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:T.label,disabled:!!B?.builtin,onChange:e=>V(`label`,e.target.value),placeholder:o(`agentLabelPlaceholder`)})]})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`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:T.description,disabled:!!B?.builtin,onChange:e=>V(`description`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`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:T.systemPrompt,disabled:!!B?.builtin,onChange:e=>V(`systemPrompt`,e.target.value)})]}),(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`div`,{className:`mb-2 text-sm font-medium text-foreground`,children:o(`allowedTools`)}),(0,m.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2`,children:i.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:!!B?.builtin,checked:T.allowedTools.includes(e.name),onChange:()=>H(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:o(`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:[o(`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:T.maxRuntimeMs,disabled:!!B?.builtin,onChange:e=>V(`maxRuntimeMs`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[o(`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:T.maxToolCalls,disabled:!!B?.builtin,onChange:e=>V(`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:T.enabledAsSubagent,disabled:!!B?.builtin,onChange:e=>V(`enabledAsSubagent`,e.target.checked)}),o(`enabledAsSubagent`)]}),L?(0,m.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:L}):null]})}),(0,m.jsx)(`div`,{className:`shrink-0 border-t border-border px-5 py-4`,children:(0,m.jsxs)(`div`,{className:`flex justify-end gap-2`,children:[(0,m.jsx)(a,{variant:`outline`,onClick:G,disabled:D||j,children:o(`cancel`)}),(0,m.jsx)(a,{onClick:q,disabled:D||j||!!B?.builtin||!v(T),children:o(`save`)})]})})]})}):null]})}export{b as AgentProfilesPage};