@shawnstack/quickforge 1.6.12 → 1.6.13

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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/AgentProfilesPage-Be0oZDW5.js +1 -0
  3. package/dist/assets/ChatPanelHost-CxEotTsy.js +291 -0
  4. package/dist/assets/{PluginsPage-qXTjDrpd.js → PluginsPage-DZlqGFLn.js} +1 -1
  5. package/dist/assets/ScheduledTasksPage-DTkj-0O4.js +2 -0
  6. package/dist/assets/{SettingsWorkspacePage-CnFDmGvW.js → SettingsWorkspacePage-BJq11OPD.js} +397 -403
  7. package/dist/assets/{SharedConversationPage-CkQ-XwYS.js → SharedConversationPage-BCR8xzii.js} +1 -1
  8. package/dist/assets/TerminalDock-Bs5b-YNp.js +2 -0
  9. package/dist/assets/WorkspaceInspector-BP0XyAlE.js +13 -0
  10. package/dist/assets/icons-DAxUA0e-.js +1 -0
  11. package/dist/assets/index-BZC61KZg.css +3 -0
  12. package/dist/assets/index-DGQbvw7v.js +63 -0
  13. package/dist/assets/{mcp-servers-dialog-CsRQxx7A.js → mcp-servers-dialog-BHljnwYv.js} +2 -2
  14. package/dist/assets/{monaco-CdOyGlWD.js → monaco-DEQoYzYI.js} +1 -1
  15. package/dist/assets/{react-vendor-VqdnQHHS.js → react-vendor-CZsiwuxm.js} +1 -1
  16. package/dist/assets/{skills-dialog-CmVLi_h6.js → skills-dialog-8rn1XU9U.js} +1 -1
  17. package/dist/index.html +6 -6
  18. package/dist/licenses/material-icon-theme.txt +8 -0
  19. package/package.json +1 -1
  20. package/server/agent-manager.mjs +163 -25
  21. package/server/custom-commands.mjs +17 -0
  22. package/server/image-generation.mjs +130 -0
  23. package/server/index.mjs +6 -0
  24. package/server/provider-config.mjs +78 -0
  25. package/server/routes/agent.mjs +9 -0
  26. package/server/routes/session-assets.mjs +49 -0
  27. package/server/routes/shared-conversation.mjs +10 -0
  28. package/server/routes/tools.mjs +1 -1
  29. package/server/routes/workspace.mjs +5 -2
  30. package/server/session-assets.mjs +134 -0
  31. package/server/storage.mjs +13 -1
  32. package/server/tools/definitions.mjs +10 -0
  33. package/server/tools/index.mjs +2 -0
  34. package/dist/assets/AgentProfilesPage-CooGLweI.js +0 -1
  35. package/dist/assets/ChatPanelHost-TEbv0lnL.js +0 -260
  36. package/dist/assets/ScheduledTasksPage-BBor8Yb2.js +0 -2
  37. package/dist/assets/TerminalDock-CvqBBMPg.js +0 -2
  38. package/dist/assets/WorkspaceInspector-6JGc6VGb.js +0 -13
  39. package/dist/assets/icons-C7j5jdKo.js +0 -1
  40. package/dist/assets/index-DxNzlPmc.js +0 -63
  41. package/dist/assets/index-HVxfqXfB.css +0 -3
@@ -0,0 +1,130 @@
1
+ import { builtinImagesModels } from '@earendil-works/pi-ai/providers/all'
2
+ import { resolveOpenRouterConfig } from './provider-config.mjs'
3
+ import { deleteSessionAsset, writeSessionAsset } from './session-assets.mjs'
4
+
5
+ export const DEFAULT_IMAGE_MODEL = 'google/gemini-2.5-flash-image'
6
+ export const MAX_GENERATED_IMAGES = 4
7
+ export const MAX_GENERATED_IMAGE_BYTES = 50 * 1024 * 1024
8
+
9
+ function requestError(message, statusCode = 400) {
10
+ const error = new Error(message)
11
+ error.statusCode = statusCode
12
+ return error
13
+ }
14
+
15
+ function normalizeBucket(value) {
16
+ if (value?.scope === 'project') return { scope: 'project', projectId: value.projectId }
17
+ return { scope: 'global' }
18
+ }
19
+
20
+ function resolveImagesModels(runtime) {
21
+ const candidate = runtime.imagesModels
22
+ if (candidate && typeof candidate.getModel === 'function') return candidate
23
+ if (typeof candidate === 'function') return candidate()
24
+ return builtinImagesModels()
25
+ }
26
+
27
+ function imageByteLength(data) {
28
+ const normalized = typeof data === 'string' ? data.replace(/\s+/g, '') : ''
29
+ if (!normalized || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
30
+ throw requestError('Image provider returned invalid base64 data')
31
+ }
32
+ const buffer = Buffer.from(normalized, 'base64')
33
+ if (buffer.toString('base64') !== normalized) {
34
+ throw requestError('Image provider returned invalid base64 data')
35
+ }
36
+ return buffer.byteLength
37
+ }
38
+
39
+ function resultText(output) {
40
+ return output
41
+ .filter((item) => item?.type === 'text' && typeof item.text === 'string' && item.text.trim())
42
+ .map((item) => item.text.trim())
43
+ .join('\n\n')
44
+ }
45
+
46
+ function contentSummary(count, text) {
47
+ const generated = `Generated ${count} image${count === 1 ? '' : 's'}.`
48
+ return text ? `${generated}\n\n${text}` : generated
49
+ }
50
+
51
+ export async function generateSessionImages(params, context = {}, runtime = {}) {
52
+ const prompt = typeof params?.prompt === 'string' ? params.prompt.trim() : ''
53
+ if (!prompt) throw requestError('prompt is required')
54
+
55
+ const sessionId = typeof context.sessionId === 'string' ? context.sessionId : params?.sessionId
56
+ if (!sessionId) throw requestError('sessionId is required')
57
+ const bucket = normalizeBucket(context.bucket || context.sessionBucket || params?.bucket || {
58
+ scope: context.scope,
59
+ projectId: context.projectId,
60
+ })
61
+ const requestedModel = typeof params?.model === 'string' && params.model.trim()
62
+ ? params.model.trim()
63
+ : DEFAULT_IMAGE_MODEL
64
+
65
+ const imagesModels = resolveImagesModels(runtime)
66
+ const model = imagesModels.getModel('openrouter', requestedModel)
67
+ if (!model || model.api !== 'openrouter-images') {
68
+ throw requestError(`Unsupported image model: ${requestedModel}`)
69
+ }
70
+
71
+ const resolveConfig = runtime.resolveOpenRouterConfig || resolveOpenRouterConfig
72
+ const config = await resolveConfig(runtime)
73
+ const requestModel = config.baseUrl ? { ...model, baseUrl: config.baseUrl } : model
74
+ const response = await imagesModels.generateImages(
75
+ requestModel,
76
+ { input: [{ type: 'text', text: prompt }] },
77
+ {
78
+ apiKey: config.apiKey,
79
+ ...(config.headers ? { headers: config.headers } : {}),
80
+ ...(runtime.signal || context.signal ? { signal: runtime.signal || context.signal } : {}),
81
+ },
82
+ )
83
+
84
+ if (response?.stopReason !== 'stop') {
85
+ throw new Error(response?.errorMessage || `Image generation stopped: ${response?.stopReason || 'unknown'}`)
86
+ }
87
+
88
+ const output = Array.isArray(response.output) ? response.output : []
89
+ const images = output.filter((item) => item?.type === 'image')
90
+ if (!images.length) throw new Error('Image generation returned no images')
91
+ if (images.length > MAX_GENERATED_IMAGES) {
92
+ throw requestError(`Image generation returned more than ${MAX_GENERATED_IMAGES} images`, 413)
93
+ }
94
+
95
+ let totalBytes = 0
96
+ for (const image of images) {
97
+ totalBytes += imageByteLength(image.data)
98
+ if (totalBytes > MAX_GENERATED_IMAGE_BYTES) {
99
+ throw requestError(`Generated images exceed the ${MAX_GENERATED_IMAGE_BYTES} byte limit`, 413)
100
+ }
101
+ }
102
+
103
+ const writeAsset = runtime.writeSessionAsset || writeSessionAsset
104
+ const deleteAsset = runtime.deleteSessionAsset || deleteSessionAsset
105
+ const assets = []
106
+ try {
107
+ for (const image of images) {
108
+ assets.push(await writeAsset(bucket, sessionId, image))
109
+ }
110
+ } catch (error) {
111
+ await Promise.allSettled(assets.map((asset) => deleteAsset(bucket, sessionId, asset.assetId)))
112
+ throw error
113
+ }
114
+
115
+ const text = resultText(output)
116
+ return {
117
+ content: contentSummary(assets.length, text),
118
+ details: {
119
+ type: 'generated_image_result',
120
+ sessionId,
121
+ prompt,
122
+ model: requestedModel,
123
+ assets,
124
+ text,
125
+ usage: response.usage,
126
+ },
127
+ }
128
+ }
129
+
130
+ export const generateImages = generateSessionImages
package/server/index.mjs CHANGED
@@ -23,6 +23,7 @@ import { handleBackupApi } from './routes/backup.mjs'
23
23
  import { handleSystemApi } from './routes/system.mjs'
24
24
  import { handleSharesApi } from './routes/shares.mjs'
25
25
  import { handleSharedConversationApi } from './routes/shared-conversation.mjs'
26
+ import { handleSessionAssetsApi } from './routes/session-assets.mjs'
26
27
  import { handleLanAccessApi, renderLanUnlockPage } from './routes/lan-access.mjs'
27
28
  import { handleMcpApi } from './routes/mcp.mjs'
28
29
  import { handlePluginsApi } from './routes/plugins.mjs'
@@ -288,6 +289,11 @@ async function handleApi(req, res, url) {
288
289
  return
289
290
  }
290
291
 
292
+ if (pathname.startsWith('/api/session-assets/')) {
293
+ await handleSessionAssetsApi(req, res, url)
294
+ return
295
+ }
296
+
291
297
  if (pathname === '/api/lan-access/status' || pathname === '/api/lan-access/settings' || pathname === '/api/lan-access/unlock' || pathname === '/api/lan-access/logout' || pathname === '/api/lan-access/revoke-all') {
292
298
  await handleLanAccessApi(req, res, url, {
293
299
  port,
@@ -0,0 +1,78 @@
1
+ import { readStore } from './storage.mjs'
2
+
3
+ const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'
4
+
5
+ function requestError(message, statusCode = 400) {
6
+ const error = new Error(message)
7
+ error.statusCode = statusCode
8
+ return error
9
+ }
10
+
11
+ function providersFromStore(store) {
12
+ return Array.isArray(store)
13
+ ? store
14
+ : Object.entries(store || {}).map(([id, provider]) => (
15
+ provider && typeof provider === 'object' ? { id, ...provider } : provider
16
+ ))
17
+ }
18
+
19
+ function isOpenRouterProvider(provider) {
20
+ if (!provider || typeof provider !== 'object') return false
21
+ const id = String(provider.id || '').trim().toLowerCase()
22
+ const name = String(provider.name || '').trim().toLowerCase()
23
+ if (id === 'openrouter' || name === 'openrouter') return true
24
+
25
+ try {
26
+ return new URL(String(provider.baseUrl || '')).hostname.toLowerCase() === 'openrouter.ai'
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ function normalizeHeaders(provider) {
33
+ const source = provider?.headers || provider?.models?.find((model) => model?.headers)?.headers
34
+ if (!source || typeof source !== 'object' || Array.isArray(source)) return undefined
35
+ const headers = {}
36
+ for (const [key, value] of Object.entries(source)) {
37
+ if (typeof value === 'string' && key.trim()) headers[key.trim()] = value
38
+ }
39
+ return Object.keys(headers).length ? headers : undefined
40
+ }
41
+
42
+ function findProviderKey(keys, providerName) {
43
+ if (!keys || typeof keys !== 'object' || Array.isArray(keys)) return undefined
44
+ if (typeof keys[providerName] === 'string' && keys[providerName].trim()) return keys[providerName].trim()
45
+
46
+ const wanted = String(providerName || '').trim().toLowerCase()
47
+ for (const [key, value] of Object.entries(keys)) {
48
+ const normalized = key.trim().toLowerCase()
49
+ if ((normalized === wanted || normalized === 'openrouter') && typeof value === 'string' && value.trim()) {
50
+ return value.trim()
51
+ }
52
+ }
53
+ return undefined
54
+ }
55
+
56
+ export async function resolveOpenRouterConfig(runtime = {}) {
57
+ const read = runtime.readStore || readStore
58
+ const [providerStore, providerKeys] = await Promise.all([
59
+ read('custom-providers'),
60
+ read('provider-keys'),
61
+ ])
62
+ const provider = providersFromStore(providerStore).find(isOpenRouterProvider)
63
+ if (!provider) throw requestError('OpenRouter provider is not configured')
64
+
65
+ const providerName = String(provider.name || provider.id || 'openrouter').trim()
66
+ const apiKey = findProviderKey(providerKeys, providerName)
67
+ if (!apiKey) throw requestError('OpenRouter API key is not configured')
68
+
69
+ const baseUrl = typeof provider.baseUrl === 'string' && provider.baseUrl.trim()
70
+ ? provider.baseUrl.trim().replace(/\/$/, '')
71
+ : DEFAULT_OPENROUTER_BASE_URL
72
+
73
+ return {
74
+ apiKey,
75
+ baseUrl,
76
+ headers: normalizeHeaders(provider),
77
+ }
78
+ }
@@ -16,6 +16,7 @@ import {
16
16
  touchSession,
17
17
  listSessions,
18
18
  updateSessionAccessMode,
19
+ updateSessionTitle,
19
20
  updateSessionYoloMode,
20
21
  updateSessionModel,
21
22
  updateSessionThinkingLevel,
@@ -153,6 +154,14 @@ export async function handleAgentApi(req, res, url) {
153
154
  return
154
155
  }
155
156
 
157
+ // POST /api/agents/:sessionId/title — update the authoritative session title
158
+ if (req.method === 'POST' && subPath === 'title') {
159
+ const body = await readJsonBody(req)
160
+ const result = await updateSessionTitle(sessionId, body?.title)
161
+ sendJson(res, 200, result)
162
+ return
163
+ }
164
+
156
165
  // POST /api/agents/:sessionId/access-mode — update session Agent access mode
157
166
  if (req.method === 'POST' && subPath === 'access-mode') {
158
167
  const body = await readJsonBody(req)
@@ -0,0 +1,49 @@
1
+ import { findSessionBucket } from '../storage.mjs'
2
+ import { readSessionAsset } from '../session-assets.mjs'
3
+ import { decodeSegment } from '../utils/response.mjs'
4
+
5
+ function notFound(message = 'Image asset not found') {
6
+ const error = new Error(message)
7
+ error.statusCode = 404
8
+ return error
9
+ }
10
+
11
+ export async function sendSessionAsset(res, bucket, sessionId, assetId) {
12
+ let asset
13
+ try {
14
+ asset = await readSessionAsset(bucket, sessionId, assetId)
15
+ } catch (error) {
16
+ if (error?.code === 'ENOENT') throw notFound()
17
+ throw error
18
+ }
19
+
20
+ res.writeHead(200, {
21
+ 'content-type': asset.mimeType,
22
+ 'content-length': String(asset.size),
23
+ 'cache-control': 'private, max-age=31536000, immutable',
24
+ 'content-disposition': 'inline',
25
+ 'x-content-type-options': 'nosniff',
26
+ })
27
+ res.end(asset.data)
28
+ }
29
+
30
+ export async function handleSessionAssetsApi(req, res, url) {
31
+ if (req.method !== 'GET') {
32
+ const error = new Error('Session asset endpoints require GET')
33
+ error.statusCode = 405
34
+ throw error
35
+ }
36
+
37
+ const parts = url.pathname.split('/').filter(Boolean)
38
+ const sessionId = decodeSegment(parts[2])
39
+ const assetId = decodeSegment(parts[3])
40
+ if (!sessionId || !assetId || parts.length !== 4) {
41
+ const error = new Error('Missing session image asset path')
42
+ error.statusCode = 400
43
+ throw error
44
+ }
45
+
46
+ const bucket = await findSessionBucket(sessionId)
47
+ if (!bucket) throw notFound('Session not found')
48
+ await sendSessionAsset(res, bucket, sessionId, assetId)
49
+ }
@@ -1,5 +1,6 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
2
  import { readSessionValue, readStore } from '../storage.mjs'
3
+ import { sendSessionAsset } from './session-assets.mjs'
3
4
  import { abortRun, restoreAgent, runPrompt, getSessionState, getSessionEventBus, updateSessionModel, updateSessionThinkingLevel } from '../agent-manager.mjs'
4
5
  import {
5
6
  assertShareActive,
@@ -307,6 +308,7 @@ export async function handleSharedConversationApi(req, res, url) {
307
308
  const parts = url.pathname.split('/').filter(Boolean)
308
309
  const shareId = decodeSegment(parts[2])
309
310
  const action = parts[3]
311
+ const actionId = parts[4]
310
312
 
311
313
  if (!shareId) {
312
314
  const error = new Error('Missing share id')
@@ -343,6 +345,14 @@ export async function handleSharedConversationApi(req, res, url) {
343
345
  const record = await requireShareAuth(req, shareId)
344
346
  if (record.permission === 'operate' && !record.passwordHash) throw passwordRequiredError()
345
347
 
348
+ if (req.method === 'GET' && action === 'assets' && actionId) {
349
+ const bucket = record.scope === 'project'
350
+ ? { scope: 'project', projectId: record.projectId }
351
+ : { scope: 'global' }
352
+ await sendSessionAsset(res, bucket, record.sessionId, decodeSegment(actionId))
353
+ return
354
+ }
355
+
346
356
  if (req.method === 'GET' && action === 'session') {
347
357
  sendJson(res, 200, await sharedSessionPayload(record))
348
358
  return
@@ -8,7 +8,7 @@ import { callPluginTool, createPluginToolDefinitions, isPluginToolName } from '.
8
8
  import { safeReadTools } from '../approval-store.mjs'
9
9
  import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
10
10
 
11
- const directRouteDisabledTools = new Set(['run_subagent', 'manage_global_memory'])
11
+ const directRouteDisabledTools = new Set(['run_subagent', 'manage_global_memory', 'generate_image'])
12
12
 
13
13
  /**
14
14
  * GET /api/tools — returns canonical tool definitions (no project context needed).
@@ -304,9 +304,12 @@ async function countWorkspaceLines(context, relativePath) {
304
304
  }
305
305
  }
306
306
 
307
- async function listGitStatus(context) {
307
+ export async function listGitStatus(context) {
308
308
  if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
309
- const result = await git(['status', '--porcelain=v1', '-z'], context.workspaceRoot)
309
+ const result = await git(
310
+ ['status', '--porcelain=v1', '-z', '--untracked-files=all'],
311
+ context.workspaceRoot,
312
+ )
310
313
  const files = parseGitStatus(result.stdout)
311
314
  const numstat = await collectNumstat(context)
312
315
  for (const file of files) {
@@ -0,0 +1,134 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { storageDir } from './storage.mjs'
5
+
6
+ export const MAX_SESSION_IMAGE_BYTES = 25 * 1024 * 1024
7
+
8
+ const MIME_TYPES = new Map([
9
+ ['image/png', 'png'],
10
+ ['image/jpeg', 'jpg'],
11
+ ['image/webp', 'webp'],
12
+ ['image/gif', 'gif'],
13
+ ])
14
+
15
+ const SAFE_SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/
16
+ const ASSET_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.(?:png|jpg|webp|gif)$/i
17
+
18
+ function requestError(message, statusCode = 400) {
19
+ const error = new Error(message)
20
+ error.statusCode = statusCode
21
+ return error
22
+ }
23
+
24
+ function assertSafeSegment(value, label) {
25
+ if (typeof value !== 'string' || !SAFE_SEGMENT_RE.test(value)) {
26
+ throw requestError(`Invalid ${label}`)
27
+ }
28
+ return value
29
+ }
30
+
31
+ function normalizeBucket(bucket) {
32
+ if (!bucket || typeof bucket !== 'object' || Array.isArray(bucket)) {
33
+ throw requestError('Invalid session asset scope')
34
+ }
35
+ if (bucket.scope === 'global') return { scope: 'global' }
36
+ if (bucket.scope === 'project') {
37
+ return {
38
+ scope: 'project',
39
+ projectId: assertSafeSegment(bucket.projectId, 'projectId'),
40
+ }
41
+ }
42
+ throw requestError('Invalid session asset scope')
43
+ }
44
+
45
+ function assetsRoot(bucket) {
46
+ if (bucket.scope === 'project') {
47
+ return path.join(storageDir, 'conversations', 'projects', bucket.projectId, 'assets')
48
+ }
49
+ return path.join(storageDir, 'conversations', 'global', 'assets')
50
+ }
51
+
52
+ function sessionAssetsDir(bucket, sessionId) {
53
+ return path.join(assetsRoot(normalizeBucket(bucket)), assertSafeSegment(sessionId, 'sessionId'))
54
+ }
55
+
56
+ function normalizeMimeType(mimeType) {
57
+ const value = typeof mimeType === 'string' ? mimeType.trim().toLowerCase() : ''
58
+ const extension = MIME_TYPES.get(value)
59
+ if (!extension) throw requestError(`Unsupported image MIME type: ${mimeType || ''}`)
60
+ return { mimeType: value, extension }
61
+ }
62
+
63
+ function decodeBase64(value) {
64
+ const normalized = typeof value === 'string' ? value.replace(/\s+/g, '') : ''
65
+ if (!normalized || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
66
+ throw requestError('Invalid base64 image data')
67
+ }
68
+ const buffer = Buffer.from(normalized, 'base64')
69
+ if (buffer.toString('base64') !== normalized) throw requestError('Invalid base64 image data')
70
+ return buffer
71
+ }
72
+
73
+ function normalizeImageData(data) {
74
+ if (Buffer.isBuffer(data)) return data
75
+ if (data instanceof Uint8Array) return Buffer.from(data)
76
+ return decodeBase64(data)
77
+ }
78
+
79
+ function mimeTypeFromAssetId(assetId) {
80
+ const extension = path.extname(assetId).slice(1).toLowerCase()
81
+ for (const [mimeType, candidate] of MIME_TYPES) {
82
+ if (candidate === extension) return mimeType
83
+ }
84
+ throw requestError('Invalid assetId')
85
+ }
86
+
87
+ function assetMetadata(assetId, mimeType, size) {
88
+ return { assetId, mimeType, size }
89
+ }
90
+
91
+ export function validateSessionAssetId(assetId) {
92
+ if (typeof assetId !== 'string' || !ASSET_ID_RE.test(assetId)) {
93
+ throw requestError('Invalid assetId')
94
+ }
95
+ return assetId
96
+ }
97
+
98
+ export async function writeSessionAsset(bucket, sessionId, image) {
99
+ const dir = sessionAssetsDir(bucket, sessionId)
100
+ const { mimeType, extension } = normalizeMimeType(image?.mimeType)
101
+ const data = normalizeImageData(image?.data)
102
+ if (data.byteLength > MAX_SESSION_IMAGE_BYTES) {
103
+ throw requestError(`Image exceeds the ${MAX_SESSION_IMAGE_BYTES} byte limit`, 413)
104
+ }
105
+
106
+ await fs.mkdir(dir, { recursive: true })
107
+ const assetId = `${randomUUID()}.${extension}`
108
+ await fs.writeFile(path.join(dir, assetId), data, { flag: 'wx' })
109
+ return assetMetadata(assetId, mimeType, data.byteLength)
110
+ }
111
+
112
+ export async function readSessionAsset(bucket, sessionId, assetId) {
113
+ const dir = sessionAssetsDir(bucket, sessionId)
114
+ const safeAssetId = validateSessionAssetId(assetId)
115
+ const data = await fs.readFile(path.join(dir, safeAssetId))
116
+ if (data.byteLength > MAX_SESSION_IMAGE_BYTES) {
117
+ throw requestError('Stored image exceeds the allowed size', 413)
118
+ }
119
+ return {
120
+ ...assetMetadata(safeAssetId, mimeTypeFromAssetId(safeAssetId), data.byteLength),
121
+ data,
122
+ }
123
+ }
124
+
125
+ export async function deleteSessionAsset(bucket, sessionId, assetId) {
126
+ const dir = sessionAssetsDir(bucket, sessionId)
127
+ const safeAssetId = validateSessionAssetId(assetId)
128
+ await fs.rm(path.join(dir, safeAssetId), { force: true })
129
+ }
130
+
131
+ export async function deleteSessionAssets(bucket, sessionId) {
132
+ const dir = sessionAssetsDir(bucket, sessionId)
133
+ await fs.rm(dir, { recursive: true, force: true })
134
+ }
@@ -517,7 +517,12 @@ async function writeSessionValues(data) {
517
517
  existingFiles.map(async (file) => {
518
518
  const sessionId = path.basename(file, '.json')
519
519
  if (!nextIds.has(sessionId)) {
520
+ const bucket = sessionBucketIndex.get(sessionId) || await findSessionBucketByDataFile(sessionId)
520
521
  await fs.rm(file, { force: true })
522
+ if (bucket) {
523
+ const { deleteSessionAssets } = await import('./session-assets.mjs')
524
+ await deleteSessionAssets(bucket, sessionId)
525
+ }
521
526
  sessionBucketIndex.delete(sessionId)
522
527
  }
523
528
  }),
@@ -574,7 +579,12 @@ export async function findSessionBucket(sessionId) {
574
579
  await ensureStorage()
575
580
  await rebuildBucketIndex()
576
581
  }
577
- return sessionBucketIndex.get(sessionId) ?? null
582
+ const indexed = sessionBucketIndex.get(sessionId)
583
+ if (indexed) return indexed
584
+
585
+ const recovered = await findSessionBucketByDataFile(sessionId)
586
+ if (recovered) sessionBucketIndex.set(sessionId, recovered)
587
+ return recovered
578
588
  }
579
589
 
580
590
  export async function readSessionValue(sessionId) {
@@ -599,6 +609,8 @@ export async function deleteSessionValue(sessionId) {
599
609
  const bucket = await findSessionBucket(sessionId)
600
610
  if (!bucket) return
601
611
  await fs.rm(sessionDataFile(sessionId, bucket), { force: true })
612
+ const { deleteSessionAssets } = await import('./session-assets.mjs')
613
+ await deleteSessionAssets(bucket, sessionId)
602
614
  sessionBucketIndex.delete(sessionId)
603
615
  })
604
616
  }
@@ -116,6 +116,16 @@ export const workspaceTools = [
116
116
  }),
117
117
  executionMode: 'sequential',
118
118
  },
119
+ {
120
+ name: 'generate_image',
121
+ label: 'Generate image',
122
+ description: 'Generate images with the configured OpenRouter provider and save them as assets owned by the current conversation. Use this when the user explicitly asks for a generated bitmap image. The operation may incur provider charges.',
123
+ parameters: Type.Object({
124
+ prompt: Type.String({ description: 'Detailed image-generation prompt.' }),
125
+ model: Type.Optional(Type.String({ description: 'Optional OpenRouter image model ID. Defaults to google/gemini-2.5-flash-image.' })),
126
+ }),
127
+ executionMode: 'sequential',
128
+ },
119
129
  {
120
130
  name: 'present_files',
121
131
  label: 'Present files',
@@ -15,6 +15,7 @@ import {
15
15
  } from '../skills.mjs'
16
16
  import { getToolWorkspaceRoot } from '../utils/workspace.mjs'
17
17
  import { manageGlobalMemory } from '../global-memory.mjs'
18
+ import { generateSessionImages } from '../image-generation.mjs'
18
19
 
19
20
  const require = createRequire(import.meta.url)
20
21
 
@@ -1100,6 +1101,7 @@ export const toolHandlers = {
1100
1101
  write_file: toolWriteFile,
1101
1102
  edit_file: toolEditFile,
1102
1103
  run_command: toolRunCommand,
1104
+ generate_image: generateSessionImages,
1103
1105
  present_files: toolPresentFiles,
1104
1106
  activate_skill: toolActivateSkill,
1105
1107
  read_skill_resource: toolReadSkillResource,
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{D as t,Ft as n,Rt as r,Vt as i,a,dt as ee,l as o}from"./icons-C7j5jdKo.js";import{i as s,n as c}from"./react-vendor-VqdnQHHS.js";import{$ as te,H as l,Q as ne,ct as u,et as re,ht as d,nt as ie,tt as ae,ut as f}from"./index-DxNzlPmc.js";var p=e(i(),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,i]=(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`)]);i(e.agents),c(t.tools)}(0,p.useEffect)(()=>{let e=!1;async function t(){try{let[t,n]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);if(e)return;i(t.agents),c(n.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 re(),n=await te(t);de(n);let r=await ae(t),i=r.model??await ie(t)??n[0];if(e)return;ue(i),pe(r.thinkingLevel??ne(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,n=e.enabledAsSubagent;i(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){i(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:n}:t)),z(t instanceof Error?t.message:d(`requestFailed`))}}async function be(e){if(!(e.builtin||e.readonly)&&await l({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)(u,{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)(u,{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)(f,{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)(f,{variant:`outline`,onClick:$,disabled:A||P,children:d(`cancel`)}),(0,m.jsx)(f,{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)(n,{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};