@shawnstack/quickforge 1.7.5 → 1.7.7

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 (67) hide show
  1. package/dist/assets/AgentProfilesPage-BfA_sQ3A.js +1 -0
  2. package/dist/assets/ChatPanelHost-Dul1ahKj.js +307 -0
  3. package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +1 -0
  4. package/dist/assets/PluginsPage-CPE5tbbf.js +1 -0
  5. package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +2 -0
  6. package/dist/assets/SettingsWorkspacePage-BE2T4d3P.js +1696 -0
  7. package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +1 -0
  8. package/dist/assets/SharedConversationPage-BK9XJnEe.js +1 -0
  9. package/dist/assets/TerminalDock-CL7HU9DI.js +2 -0
  10. package/dist/assets/{WorkspaceInspector-D9kdnLDd.js → WorkspaceInspector-MNBLIKMn.js} +5 -5
  11. package/dist/assets/{dist-DLKC_odd.js → dist-yDHbi5ux.js} +1 -1
  12. package/dist/assets/{esm-Df4edDpj.js → esm-sdsK6gaB.js} +2 -2
  13. package/dist/assets/{icons-CXPP1fFT.js → icons-2D18YbSI.js} +1 -1
  14. package/dist/assets/index-BTPzyq7d.js +65 -0
  15. package/dist/assets/index-BiCTgUlY.css +3 -0
  16. package/dist/assets/{mcp-servers-dialog-BUSPvaqI.js → mcp-servers-dialog-GY7sdfIr.js} +2 -2
  17. package/dist/assets/{logger-8REuLPGK.js → model-display-label-C6-0eDxF.js} +1 -1
  18. package/dist/assets/{monaco-D6OhXgjg.js → monaco-DvXErxp2.js} +1 -1
  19. package/dist/assets/{react-vendor-1KC5PyRx.js → react-vendor-3UaA-C9W.js} +1 -1
  20. package/dist/assets/{skills-dialog-BThnIUBs.js → skills-dialog-C1fJ1Jl7.js} +3 -3
  21. package/dist/assets/{web-DoV8MSMn.js → web-B5qgZJRv.js} +1 -1
  22. package/dist/index.html +8 -8
  23. package/dist/sw.js +17 -0
  24. package/package.json +2 -1
  25. package/runtime-assets/agent/darwin-arm64/qf-agent +0 -0
  26. package/runtime-assets/agent/darwin-x64/qf-agent +0 -0
  27. package/runtime-assets/agent/linux-arm64/qf-agent +0 -0
  28. package/runtime-assets/agent/linux-x64/qf-agent +0 -0
  29. package/runtime-assets/agent/win32-x64/qf-agent.exe +0 -0
  30. package/server/access-policy.mjs +11 -0
  31. package/server/agent-manager.mjs +349 -48
  32. package/server/agent-profiles.mjs +27 -8
  33. package/server/auto-compaction.mjs +78 -9
  34. package/server/cloud/client.mjs +4 -7
  35. package/server/cloud/config.mjs +9 -16
  36. package/server/cloud/credential-store.mjs +10 -3
  37. package/server/cloud/identity.mjs +17 -60
  38. package/server/cloud/models.mjs +1 -1
  39. package/server/cloud/qf-agent-process.mjs +571 -0
  40. package/server/cloud/runtime.mjs +11 -5
  41. package/server/cloud/service-config.mjs +8 -4
  42. package/server/context-usage.mjs +11 -4
  43. package/server/index.mjs +96 -37
  44. package/server/mcp/registry.mjs +8 -7
  45. package/server/message-converters.mjs +40 -19
  46. package/server/model-catalog.mjs +11 -4
  47. package/server/opencode-acp-agent.mjs +1151 -0
  48. package/server/public-api.mjs +11 -5
  49. package/server/routes/agent-profiles.mjs +6 -3
  50. package/server/routes/agent.mjs +43 -3
  51. package/server/routes/backup.mjs +5 -4
  52. package/server/routes/cloud.mjs +54 -35
  53. package/server/routes/shares.mjs +7 -8
  54. package/server/routes/storage.mjs +9 -24
  55. package/server/routes/system.mjs +7 -4
  56. package/server/utils/process-tree.mjs +78 -0
  57. package/dist/assets/AgentProfilesPage-XNjuqJs7.js +0 -1
  58. package/dist/assets/ChatPanelHost-CuF-PZpc.js +0 -292
  59. package/dist/assets/CloudAccountSettingsPage-GJSoi6sO.js +0 -1
  60. package/dist/assets/PluginsPage-BQgp0qN-.js +0 -1
  61. package/dist/assets/ScheduledTasksPage-DdekATzq.js +0 -2
  62. package/dist/assets/SettingsWorkspacePage-Cb54Ic91.js +0 -1639
  63. package/dist/assets/ShareLinksSettingsPage-C75WJpaF.js +0 -1
  64. package/dist/assets/SharedConversationPage-CjMtSQTW.js +0 -1
  65. package/dist/assets/TerminalDock-D4zJBJc4.js +0 -2
  66. package/dist/assets/index-_ZhK6bvp.js +0 -65
  67. package/dist/assets/index-iVV2bP_f.css +0 -3
@@ -68,7 +68,7 @@ function createVersionMismatchError(health, options = {}) {
68
68
  return error
69
69
  }
70
70
 
71
- function buildEnv(options = {}) {
71
+ export function buildEnv(options = {}) {
72
72
  const host = normalizeHost(options.host)
73
73
  const port = getPort(options.port)
74
74
  const shareLan = options.shareLan === true
@@ -84,6 +84,10 @@ function buildEnv(options = {}) {
84
84
  if (options.workspaceDir) env.QUICKFORGE_WORKSPACE_DIR = path.resolve(options.workspaceDir)
85
85
  if (options.vitePort) env.QUICKFORGE_VITE_PORT = String(options.vitePort)
86
86
  if (options.terminal === false) env.QUICKFORGE_TERMINAL = '0'
87
+ if (options.qfAgentPath) env.QUICKFORGE_QF_AGENT_PATH = path.resolve(options.qfAgentPath)
88
+ if (options.qfAgentIdentityDir) env.QUICKFORGE_QF_AGENT_IDENTITY_DIR = path.resolve(options.qfAgentIdentityDir)
89
+ if (options.qfAgentEnabled !== undefined) env.QUICKFORGE_QF_AGENT_ENABLED = options.qfAgentEnabled === false ? '0' : '1'
90
+ if (options.runtimeKind) env.QUICKFORGE_RUNTIME_KIND = String(options.runtimeKind)
87
91
  if (options.allowRemote || shareLan) env.QUICKFORGE_ALLOW_REMOTE = '1'
88
92
  delete env.ELECTRON_RUN_AS_NODE
89
93
  delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE
@@ -172,7 +176,7 @@ export async function startQuickForge(options = {}) {
172
176
 
173
177
  if (options.inline === true) {
174
178
  prepareQuickForgeEnv(options)
175
- await import(serverScriptUrl)
179
+ const serverModule = await import(serverScriptUrl)
176
180
  const health = await waitForQuickForge(options)
177
181
  if (!health) throw new Error('QuickForge failed to start: health check timed out')
178
182
  return {
@@ -184,7 +188,8 @@ export async function startQuickForge(options = {}) {
184
188
  reused: false,
185
189
  inline: true,
186
190
  async stop() {
187
- return false
191
+ await serverModule.stopQuickForgeServer()
192
+ return true
188
193
  },
189
194
  }
190
195
  }
@@ -242,8 +247,9 @@ export async function startQuickForge(options = {}) {
242
247
  }
243
248
 
244
249
  export async function stopQuickForge(instance) {
245
- if (!instance?.child || instance.reused) return false
246
- if (instance.child.killed) return false
250
+ if (!instance || instance.reused) return false
251
+ if (typeof instance.stop === 'function') return instance.stop()
252
+ if (!instance.child || instance.child.killed) return false
247
253
  instance.child.kill('SIGTERM')
248
254
  return true
249
255
  }
@@ -40,7 +40,7 @@ import {
40
40
  getAgentProfile,
41
41
  listAgentProfiles,
42
42
  listAvailableAgentTools,
43
- updateBuiltinAgentModelOverride,
43
+ updateBuiltinAgentOverrides,
44
44
  updateCustomAgentProfile,
45
45
  } from '../agent-profiles.mjs'
46
46
 
@@ -194,9 +194,12 @@ export async function handleAgentProfilesApi(req, res, url, context = {}) {
194
194
  const current = await getAgentProfile(id)
195
195
  const body = await readJsonBody(req)
196
196
  if (current?.builtin) {
197
+ const allowedKeys = ['model', 'thinkingLevel']
197
198
  const keys = Object.keys(body || {})
198
- if (keys.length !== 1 || keys[0] !== 'model') throw requestError('Built-in agents only allow model updates', 403)
199
- sendJson(res, 200, { agent: agentProfileSnapshot(await updateBuiltinAgentModelOverride(id, (await canonicalizeProfileModel(body, current, context)).model)) })
199
+ if (keys.length === 0 || keys.some((key) => !allowedKeys.includes(key))) {
200
+ throw requestError('Built-in agents only allow model and thinkingLevel updates', 403)
201
+ }
202
+ sendJson(res, 200, { agent: agentProfileSnapshot(await updateBuiltinAgentOverrides(id, await canonicalizeProfileModel(body, current, context))) })
200
203
  return
201
204
  }
202
205
  if (current?.readonly) throw requestError('Read-only agents cannot be modified from the API', 403)
@@ -3,6 +3,7 @@ import { logger } from '../utils/logger.mjs'
3
3
  import { resolveModelBinding } from '../model-catalog.mjs'
4
4
  import {
5
5
  createAgent,
6
+ validateAgentHarness,
6
7
  runPrompt,
7
8
  abortRun,
8
9
  steerAgent,
@@ -22,6 +23,9 @@ import {
22
23
  updateSessionYoloMode,
23
24
  updateSessionModel,
24
25
  updateSessionThinkingLevel,
26
+ updateSessionHarnessConfigOption,
27
+ updateSessionHarnessMode,
28
+ forkSession,
25
29
  approveToolCall,
26
30
  rejectToolCall,
27
31
  approveAutoCompact,
@@ -161,12 +165,13 @@ export async function handleAgentApi(req, res, url, context = {}) {
161
165
  // POST /api/agents/:sessionId — create/ensure agent
162
166
  if (req.method === 'POST' && parts.length === 3) {
163
167
  const body = await readJsonBody(req)
168
+ const harness = validateAgentHarness(body?.harness)
164
169
  let config
165
- if (body?.modelRef || body?.model) {
170
+ if (harness === 'quickforge' && (body?.modelRef || body?.model)) {
166
171
  const binding = await resolveModelBinding(body, { context, legacySnapshot: body?.model })
167
- config = { ...body, model: binding.model, modelRef: binding.modelRef, modelAccessContext: context, resolvePersistedModel: true }
172
+ config = { ...body, harness, model: binding.model, modelRef: binding.modelRef, modelAccessContext: context, resolvePersistedModel: true }
168
173
  } else {
169
- config = { ...body, modelAccessContext: context, resolvePersistedModel: true }
174
+ config = { ...body, harness, modelAccessContext: context, resolvePersistedModel: true }
170
175
  }
171
176
  const session = await createAgent(sessionId, config)
172
177
  sendJson(res, 200, {
@@ -175,6 +180,8 @@ export async function handleAgentApi(req, res, url, context = {}) {
175
180
  scope: session.scope,
176
181
  title: session.title,
177
182
  source: session.source || undefined,
183
+ harness: session.harness,
184
+ harnessSessionId: session.agent.harnessSessionId || session.harnessSessionId || undefined,
178
185
  channelId: session.channelId || undefined,
179
186
  channelName: session.channelName || undefined,
180
187
  accessMode: session.accessMode,
@@ -243,6 +250,39 @@ export async function handleAgentApi(req, res, url, context = {}) {
243
250
  return
244
251
  }
245
252
 
253
+ // POST /api/agents/:sessionId/harness/config-option — update an advertised Harness config option
254
+ if (req.method === 'POST' && subPath === 'harness/config-option') {
255
+ const body = await readJsonBody(req)
256
+ if (typeof body?.configId !== 'string' || !body.configId || body.value === undefined) {
257
+ const error = new Error('Missing configId or value in request body')
258
+ error.statusCode = 400
259
+ throw error
260
+ }
261
+ const result = await updateSessionHarnessConfigOption(sessionId, body.configId, body.value)
262
+ sendJson(res, 200, result)
263
+ return
264
+ }
265
+
266
+ // POST /api/agents/:sessionId/harness/mode — update an advertised Harness mode
267
+ if (req.method === 'POST' && subPath === 'harness/mode') {
268
+ const body = await readJsonBody(req)
269
+ if (typeof body?.modeId !== 'string' || !body.modeId) {
270
+ const error = new Error('Missing modeId in request body')
271
+ error.statusCode = 400
272
+ throw error
273
+ }
274
+ const result = await updateSessionHarnessMode(sessionId, body.modeId)
275
+ sendJson(res, 200, result)
276
+ return
277
+ }
278
+
279
+ // POST /api/agents/:sessionId/fork — fork the entire current OpenCode session
280
+ if (req.method === 'POST' && subPath === 'fork') {
281
+ const result = await forkSession(sessionId)
282
+ sendJson(res, 200, result)
283
+ return
284
+ }
285
+
246
286
  // POST /api/agents/:sessionId/rollback — roll back from a message index on the authoritative server state
247
287
  if (req.method === 'POST' && subPath === 'rollback') {
248
288
  const body = await readJsonBody(req)
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { promises as fs } from 'node:fs'
3
3
  import path from 'node:path'
4
+ import { isAuthenticatedAppClient } from '../access-policy.mjs'
4
5
  import { sendJson, readJsonBody } from '../utils/response.mjs'
5
6
  import {
6
7
  ensureStorage,
@@ -580,11 +581,11 @@ async function restoreValidatedBackup(backup, mode = 'replace') {
580
581
  return summary
581
582
  }
582
583
 
583
- export async function handleBackupApi(req, res, url, context = {}) {
584
- if (context.isLocalRequest === false) {
585
- const error = new Error('Backup import and export are available only on this computer.')
584
+ export async function handleBackupApi(req, res, url, context = { isLocalRequest: true }) {
585
+ if (!isAuthenticatedAppClient(context)) {
586
+ const error = new Error('Backup import and export require a local or authenticated remote client.')
586
587
  error.statusCode = 403
587
- error.errorCode = 'backup_local_only'
588
+ error.errorCode = 'backup_auth_required'
588
589
  throw error
589
590
  }
590
591
  if (req.method === 'GET' && url.pathname === '/api/backup/export') {
@@ -1,3 +1,5 @@
1
+ import { getQfAgentStatus } from '../cloud/qf-agent-process.mjs'
2
+ import { isAuthenticatedAppClient } from '../access-policy.mjs'
1
3
  import { CloudClient, CloudApiError } from '../cloud/client.mjs'
2
4
  import { parseCloudBaseUrl } from '../cloud/config.mjs'
3
5
  import { createCloudCredentialStore } from '../cloud/credential-store.mjs'
@@ -7,7 +9,6 @@ import {
7
9
  readCloudServiceConfig,
8
10
  saveCloudServiceConfig,
9
11
  } from '../cloud/service-config.mjs'
10
- import { isTailscaleAddress } from '../utils/network.mjs'
11
12
  import { readJsonBody, sendJson } from '../utils/response.mjs'
12
13
 
13
14
  const CLOUD_CONFIG_BODY_MAX_BYTES = 16 * 1024
@@ -79,6 +80,8 @@ export function createCloudRouteHandler({
79
80
  credentialStoreFactory = createCloudCredentialStore,
80
81
  invalidateRuntime = invalidateCloudRuntime,
81
82
  cloudClientFactory = (config) => new CloudClient(config),
83
+ qfAgentStatus = getQfAgentStatus,
84
+ onServiceConfigChanged,
82
85
  } = {}) {
83
86
  async function runtimeOrError() {
84
87
  try {
@@ -88,19 +91,23 @@ export function createCloudRouteHandler({
88
91
  }
89
92
  }
90
93
 
91
- return async function handleCloudApi(req, res, url, {
92
- isLocalRequest = false,
93
- remoteAddress,
94
- remoteAuthorized = false,
95
- } = {}) {
96
- const tailscaleAllowed = remoteAuthorized === true
97
- && isTailscaleAddress(remoteAddress)
98
- if (!isLocalRequest && !tailscaleAllowed) {
99
- throw routeError('QuickForge Cloud is available only on this computer or an authorized Tailscale client.', 403, 'cloud_local_only')
94
+ return async function handleCloudApi(req, res, url, context = {}) {
95
+ if (!isAuthenticatedAppClient(context)) {
96
+ throw routeError('QuickForge Cloud is available only on this computer or an authenticated remote client.', 403, 'cloud_local_only')
100
97
  }
101
98
 
102
99
  requireProtectedCloudWrite(req, url.pathname)
103
100
 
101
+ if (req.method === 'GET' && url.pathname === '/api/cloud/remote/status') {
102
+ const service = await readServiceConfig({ strict: false })
103
+ if (!service.enabled) {
104
+ sendJson(res, 200, { ...qfAgentStatus(), enabled: false, status: 'disabled', serverUrl: null, pid: null, verificationUriComplete: null, error: null })
105
+ return
106
+ }
107
+ sendJson(res, 200, qfAgentStatus())
108
+ return
109
+ }
110
+
104
111
  if (req.method === 'GET' && url.pathname === '/api/cloud/config') {
105
112
  sendJson(res, 200, publicCloudServiceConfig(await readServiceConfig({ strict: false })))
106
113
  return
@@ -125,19 +132,30 @@ export function createCloudRouteHandler({
125
132
 
126
133
  if (req.method === 'PUT' && url.pathname === '/api/cloud/config') {
127
134
  const body = await readJsonBody(req, CLOUD_CONFIG_BODY_MAX_BYTES)
128
- const nextUrl = parseCloudBaseUrl(body?.cloudUrl).href
129
- const credentialStore = credentialStoreFactory()
130
- const identityRecord = await credentialStore.read()
131
- if (identityRecord.refreshToken || identityRecord.pendingDeviceFlow) {
132
- const boundUrl = String(identityRecord.pendingDeviceFlow?.sessionCloudUrl || identityRecord.sessionCloudUrl || '').trim()
133
- const sameUrl = boundUrl ? sameCloudUrl(boundUrl, nextUrl) : false
134
- if (!sameUrl) {
135
- throw routeError('A QuickForge Cloud session is active. Sign out first, or explicitly rebuild the identity before switching services.', 409, 'cloud_session_active')
135
+ if (body?.enabled !== undefined && typeof body.enabled !== 'boolean') {
136
+ throw routeError('QuickForge Cloud enabled must be a boolean.', 400, 'cloud_enabled_invalid')
137
+ }
138
+ const currentConfig = await readServiceConfig({ strict: false })
139
+ const nextUrl = parseCloudBaseUrl(body?.cloudUrl === undefined ? currentConfig.cloudUrl : body.cloudUrl).href
140
+ const nextEnabled = body?.enabled === undefined ? currentConfig.enabled === true : body.enabled
141
+ const urlChanged = !sameCloudUrl(currentConfig.cloudUrl, nextUrl)
142
+ if (body?.cloudUrl !== undefined) {
143
+ const credentialStore = credentialStoreFactory()
144
+ const identityRecord = await credentialStore.read()
145
+ if (identityRecord.refreshToken || identityRecord.pendingDeviceFlow) {
146
+ const boundUrl = String(identityRecord.pendingDeviceFlow?.sessionCloudUrl || identityRecord.sessionCloudUrl || '').trim()
147
+ const sameUrl = boundUrl ? sameCloudUrl(boundUrl, nextUrl) : false
148
+ if (!sameUrl) {
149
+ throw routeError('A QuickForge Cloud session is active. Sign out first, or explicitly rebuild the identity before switching services.', 409, 'cloud_session_active')
150
+ }
136
151
  }
137
152
  }
138
- await saveServiceConfig(nextUrl)
153
+ await saveServiceConfig({ cloudUrl: nextUrl, enabled: nextEnabled })
139
154
  invalidateRuntime()
140
- sendJson(res, 200, publicCloudServiceConfig(await readServiceConfig({ strict: false })))
155
+ const saved = await readServiceConfig({ strict: false })
156
+ const notify = context.onCloudServiceConfigChanged || onServiceConfigChanged
157
+ if (typeof notify === 'function') await notify(saved, { urlChanged })
158
+ sendJson(res, 200, publicCloudServiceConfig(saved))
141
159
  return
142
160
  }
143
161
 
@@ -145,7 +163,7 @@ export function createCloudRouteHandler({
145
163
  const body = await readJsonBody(req, CLOUD_CONFIG_BODY_MAX_BYTES)
146
164
  requireDangerousConfirmation(body)
147
165
  const { runtime } = await runtimeOrError()
148
- if (runtime?.enabled) await runtime.identity.resetIdentity()
166
+ if (runtime?.identity) await runtime.identity.resetIdentity()
149
167
  else {
150
168
  const store = credentialStoreFactory()
151
169
  await store.rotateInstallation()
@@ -158,25 +176,32 @@ export function createCloudRouteHandler({
158
176
  const { runtime: current, configurationError } = await runtimeOrError()
159
177
  if (req.method === 'GET' && url.pathname === '/api/cloud/status') {
160
178
  if (configurationError) {
161
- sendJson(res, 200, { configured: false, mode: 'local', configurationError: configurationError.message })
179
+ sendJson(res, 200, { configured: false, enabled: false, mode: 'local', configurationError: configurationError.message })
162
180
  return
163
181
  }
164
182
  if (!current?.enabled) {
165
- sendJson(res, 200, { configured: false, mode: 'local' })
183
+ const localStatus = current?.identity ? await current.identity.status() : current?.store ? await current.store.readPublic() : { mode: 'local' }
184
+ sendJson(res, 200, { ...localStatus, configured: Boolean(current?.config?.baseUrl), enabled: false })
166
185
  return
167
186
  }
168
- sendJson(res, 200, { configured: true, ...(await current.identity.status()) })
187
+ sendJson(res, 200, { configured: true, enabled: true, ...(await current.identity.status()) })
169
188
  return
170
189
  }
171
190
 
172
191
  if (configurationError) throw routeError(configurationError.message, 503, 'cloud_configuration_error')
173
- if (!current?.enabled) throw routeError('QuickForge Cloud is not configured.', 503, 'cloud_not_configured')
192
+
193
+ if (req.method === 'POST' && url.pathname === '/api/cloud/logout') {
194
+ if (current?.identity) await current.identity.logout()
195
+ else if (current?.store) await current.store.clearSession({ rotateInstallationBeforeRegistration: true })
196
+ else await credentialStoreFactory().clearSession({ rotateInstallationBeforeRegistration: true })
197
+ invalidateRuntime()
198
+ sendJson(res, 200, { ok: true, mode: 'local' })
199
+ return
200
+ }
201
+
202
+ if (!current?.enabled) throw routeError('QuickForge Cloud is disabled.', 503, 'cloud_disabled')
174
203
 
175
204
  try {
176
- if (req.method === 'POST' && url.pathname === '/api/cloud/guest/start') {
177
- sendJson(res, 201, await current.identity.startGuest())
178
- return
179
- }
180
205
  if (req.method === 'POST' && url.pathname === '/api/cloud/device/start') {
181
206
  await readJsonBody(req, CLOUD_CONFIG_BODY_MAX_BYTES)
182
207
  sendJson(res, 201, await current.identity.startDeviceFlow())
@@ -204,12 +229,6 @@ export function createCloudRouteHandler({
204
229
  sendJson(res, 200, await current.identity.installations())
205
230
  return
206
231
  }
207
- if (req.method === 'POST' && url.pathname === '/api/cloud/logout') {
208
- await current.identity.logout()
209
- invalidateRuntime()
210
- sendJson(res, 200, { ok: true, mode: 'local' })
211
- return
212
- }
213
232
  const revokeMatch = url.pathname.match(/^\/api\/cloud\/installations\/([^/]+)$/)
214
233
  if (req.method === 'DELETE' && revokeMatch) {
215
234
  await current.identity.revokeInstallation(decodeURIComponent(revokeMatch[1]))
@@ -1,6 +1,7 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
2
  import { readSessionValue } from '../storage.mjs'
3
- import { getLanUrls, isTailscaleAddress } from '../utils/network.mjs'
3
+ import { getLanUrls } from '../utils/network.mjs'
4
+ import { isAuthenticatedAppClient } from '../access-policy.mjs'
4
5
  import {
5
6
  createConversationShare,
6
7
  deleteConversationShare,
@@ -28,7 +29,7 @@ function shareUrlForRequest(req, shareId, port) {
28
29
  return `${baseUrl}/share/${encodeURIComponent(shareId)}`
29
30
  }
30
31
 
31
- export async function handleSharesApi(req, res, url, context = {}) {
32
+ export async function handleSharesApi(req, res, url, context = { isLocalRequest: true }) {
32
33
  const parts = url.pathname.split('/').filter(Boolean)
33
34
 
34
35
  if (req.method === 'GET' && url.pathname === '/api/shares') {
@@ -51,9 +52,8 @@ export async function handleSharesApi(req, res, url, context = {}) {
51
52
  const password = passwordProvided ? body.password.trim() : undefined
52
53
  const expiresAt = typeof body?.expiresAt === 'string' && body.expiresAt ? body.expiresAt : undefined
53
54
  const allowCloudUsage = permission === 'operate' && body?.allowCloudUsage === true
54
- if (allowCloudUsage && context.isLocalRequest === false
55
- && !(context.remoteAuthorized === true && isTailscaleAddress(context.remoteAddress))) {
56
- const error = new Error('QuickForge Cloud sharing can only be enabled locally or from an authorized Tailscale client.')
55
+ if (allowCloudUsage && !isAuthenticatedAppClient(context)) {
56
+ const error = new Error('QuickForge Cloud sharing can only be enabled locally or from an authenticated remote client.')
57
57
  error.statusCode = 403
58
58
  throw error
59
59
  }
@@ -134,9 +134,8 @@ export async function handleSharesApi(req, res, url, context = {}) {
134
134
  const password = typeof body?.password === 'string' ? body.password : undefined
135
135
  const expiresAt = typeof body?.expiresAt === 'string' && body.expiresAt ? body.expiresAt : undefined
136
136
  const allowCloudUsage = body?.allowCloudUsage
137
- if (allowCloudUsage === true && context.isLocalRequest === false
138
- && !(context.remoteAuthorized === true && isTailscaleAddress(context.remoteAddress))) {
139
- const error = new Error('QuickForge Cloud sharing can only be enabled locally or from an authorized Tailscale client.')
137
+ if (allowCloudUsage === true && !isAuthenticatedAppClient(context)) {
138
+ const error = new Error('QuickForge Cloud sharing can only be enabled locally or from an authenticated remote client.')
140
139
  error.statusCode = 403
141
140
  throw error
142
141
  }
@@ -2,6 +2,7 @@ import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
2
  import { readStore, writeStore, atomicUpdate, getComparable, getStoreRevision, readSessionStoreScoped, readSessionValue, writeSessionValue, deleteSessionValue, ensureStorage, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
3
3
  import { AUTO_ARCHIVE_SETTINGS_KEY, archiveInactiveSessions, normalizeAutoArchiveSettings } from '../auto-archive.mjs'
4
4
  import { directorySize } from '../utils/workspace.mjs'
5
+ import { isAuthenticatedAppClient } from '../access-policy.mjs'
5
6
 
6
7
  const metadataIndexCache = new Map()
7
8
  const MAX_METADATA_INDEX_CACHE_ENTRIES = 50
@@ -94,7 +95,14 @@ async function readIndexedValues(store, indexName, direction, scope, projectId,
94
95
  return values
95
96
  }
96
97
 
97
- export async function handleStorageApi(req, res, url, context = {}) {
98
+ export async function handleStorageApi(req, res, url, context = { isLocalRequest: true }) {
99
+ if (!isAuthenticatedAppClient(context)) {
100
+ const error = new Error('Storage access requires a local or authenticated remote client.')
101
+ error.statusCode = 403
102
+ error.errorCode = 'storage_auth_required'
103
+ throw error
104
+ }
105
+
98
106
  const parts = url.pathname.split('/').filter(Boolean)
99
107
 
100
108
  if (req.method === 'GET' && url.pathname === '/api/storage/quota') {
@@ -116,29 +124,6 @@ export async function handleStorageApi(req, res, url, context = {}) {
116
124
  }
117
125
 
118
126
  const store = decodeSegment(parts[2])
119
- const remotelySensitiveStores = new Set([
120
- 'provider-keys',
121
- 'custom-providers',
122
- 'mcp',
123
- 'plugins',
124
- 'scheduled-tasks',
125
- 'agent-profile-overrides',
126
- 'custom-agents',
127
- ])
128
- if (context.isLocalRequest === false && remotelySensitiveStores.has(store)) {
129
- const error = new Error('This storage area is available only on this computer.')
130
- error.statusCode = 403
131
- error.errorCode = 'storage_local_only'
132
- throw error
133
- }
134
-
135
- const remotelyReadOnlyStores = new Set(['sessions', 'sessions-metadata'])
136
- if (context.isLocalRequest === false && remotelyReadOnlyStores.has(store) && req.method !== 'GET') {
137
- const error = new Error('Remote clients must modify conversations through the Agent API.')
138
- error.statusCode = 403
139
- error.errorCode = 'storage_remote_read_only'
140
- throw error
141
- }
142
127
 
143
128
  if (req.method === 'GET' && parts[3] === 'keys') {
144
129
  const prefix = url.searchParams.get('prefix') || ''
@@ -1,5 +1,6 @@
1
1
  import { sendJson, readJsonBody } from '../utils/response.mjs'
2
2
  import { getLanUrls } from '../utils/network.mjs'
3
+ import { isAuthenticatedAppClient } from '../access-policy.mjs'
3
4
 
4
5
  export async function handleSystemApi(req, res, url, context) {
5
6
  if (req.method === 'GET' && url.pathname === '/api/system/about') {
@@ -18,9 +19,10 @@ export async function handleSystemApi(req, res, url, context) {
18
19
  }
19
20
 
20
21
  if (req.method === 'POST' && url.pathname === '/api/system/update') {
21
- if (!context.isLocalRequest) {
22
- const error = new Error('Update is only allowed from this computer')
22
+ if (!isAuthenticatedAppClient(context)) {
23
+ const error = new Error('Update requires a local or authenticated remote client')
23
24
  error.statusCode = 403
25
+ error.errorCode = 'system_update_requires_auth'
24
26
  throw error
25
27
  }
26
28
 
@@ -36,9 +38,10 @@ export async function handleSystemApi(req, res, url, context) {
36
38
  }
37
39
 
38
40
  if (req.method === 'POST' && url.pathname === '/api/system/restart') {
39
- if (!context.isLocalRequest) {
40
- const error = new Error('Restart is only allowed from this computer')
41
+ if (!isAuthenticatedAppClient(context)) {
42
+ const error = new Error('Restart requires a local or authenticated remote client')
41
43
  error.statusCode = 403
44
+ error.errorCode = 'system_restart_requires_auth'
42
45
  throw error
43
46
  }
44
47
 
@@ -0,0 +1,78 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { once } from 'node:events'
3
+
4
+ const DEFAULT_GRACE_MS = 1000
5
+ const DEFAULT_FORCE_WAIT_MS = 1000
6
+
7
+ function isRunning(child) {
8
+ return Boolean(child?.pid) && child.exitCode == null && child.signalCode == null
9
+ }
10
+
11
+ function waitForExit(child, timeoutMs, setTimer = setTimeout, clearTimer = clearTimeout) {
12
+ if (!isRunning(child)) return Promise.resolve(true)
13
+ return new Promise((resolve) => {
14
+ let settled = false
15
+ const finish = (exited) => {
16
+ if (settled) return
17
+ settled = true
18
+ clearTimer(timer)
19
+ child.removeListener?.('exit', onExit)
20
+ resolve(exited)
21
+ }
22
+ const onExit = () => finish(true)
23
+ const timer = setTimer(() => finish(!isRunning(child)), Math.max(0, timeoutMs))
24
+ timer?.unref?.()
25
+ child.once?.('exit', onExit)
26
+ })
27
+ }
28
+
29
+ function runTaskkill(pid, force, spawnImpl = spawn) {
30
+ return new Promise((resolve) => {
31
+ const args = ['/PID', String(pid), '/T', ...(force ? ['/F'] : [])]
32
+ let command
33
+ try {
34
+ command = spawnImpl('taskkill.exe', args, { windowsHide: true, stdio: 'ignore' })
35
+ } catch {
36
+ resolve(false)
37
+ return
38
+ }
39
+ command.once?.('error', () => resolve(false))
40
+ command.once?.('exit', (code) => resolve(code === 0))
41
+ })
42
+ }
43
+
44
+ export async function signalProcessTree(child, signal, options = {}) {
45
+ if (!child?.pid) return false
46
+ const platform = options.platform ?? process.platform
47
+ if (platform === 'win32') {
48
+ return runTaskkill(child.pid, signal === 'SIGKILL', options.spawnImpl)
49
+ }
50
+
51
+ const killImpl = options.killImpl ?? process.kill
52
+ try {
53
+ killImpl(-child.pid, signal)
54
+ return true
55
+ } catch {
56
+ try {
57
+ return child.kill?.(signal) !== false
58
+ } catch {
59
+ return false
60
+ }
61
+ }
62
+ }
63
+
64
+ export async function terminateProcessTree(child, options = {}) {
65
+ if (!isRunning(child)) return
66
+ const graceMs = options.graceMs ?? DEFAULT_GRACE_MS
67
+ const forceWaitMs = options.forceWaitMs ?? DEFAULT_FORCE_WAIT_MS
68
+ const timerOptions = { setTimer: options.setTimer, clearTimer: options.clearTimer }
69
+
70
+ await signalProcessTree(child, 'SIGTERM', options)
71
+ if (await waitForExit(child, graceMs, timerOptions.setTimer, timerOptions.clearTimer)) return
72
+
73
+ await signalProcessTree(child, 'SIGKILL', options)
74
+ await Promise.race([
75
+ once(child, 'exit').catch(() => {}),
76
+ waitForExit(child, forceWaitMs, timerOptions.setTimer, timerOptions.clearTimer),
77
+ ])
78
+ }
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{A as t,At as n,Ft as r,Vt as i,Wt as a,dt as o,o as s,u as ee}from"./icons-CXPP1fFT.js";import{i as c,n as l}from"./react-vendor-1KC5PyRx.js";import{n as u,t as te}from"./model-reference-o41fJ6jQ.js";import{B as d,Gt as ne,Jt as re,Kt as ie,Yt as ae,dn as f,in as oe,on as p,ot as se,tn as m,z as ce}from"./index-_ZhK6bvp.js";import{i as le,r as ue,t as de}from"./model-identity-CgA6_Pbm.js";var h=e(a(),1),fe=c(),g=l(),_=144,v=82,pe=4,y=8;function b(e){return JSON.stringify(u(e))}function x(e){if(!e)return{mode:`inherit`};try{return{mode:`fixed`,...JSON.parse(e)}}catch{return{mode:`inherit`}}}function S(e){return!e||e.mode!==`fixed`?``:e.source===`cloud`?JSON.stringify({version:1,source:`cloud`,catalogId:e.catalogId}):e.source===`custom`?JSON.stringify({version:1,source:`custom`,providerId:e.providerId,modelId:e.modelId}):JSON.stringify({version:1,source:`legacy-custom`,provider:e.provider,modelId:e.modelId,...e.api?{api:e.api}:{},...e.baseUrl?{baseUrl:e.baseUrl}:{}})}function C(){return{name:``,label:``,description:``,systemPrompt:``,allowedTools:[`read_file`,`grep_files`],maxRuntimeMs:`3600000`,maxToolCalls:`300`,enabledAsSubagent:!0,modelMode:`inherit`,fixedModelValue:``,thinkingLevel:`inherit`}}function me(e){return{name:e.name,label:e.label,description:e.description??``,systemPrompt:e.systemPrompt??``,allowedTools:e.allowedTools??[],maxRuntimeMs:String(e.maxRuntimeMs??36e5),maxToolCalls:String(e.maxToolCalls??300),enabledAsSubagent:e.enabledAsSubagent,modelMode:e.model?.mode===`fixed`?`fixed`:`inherit`,fixedModelValue:S(e.model),thinkingLevel:e.thinkingLevel??`inherit`}}function he(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||36e5),maxToolCalls:Number(e.maxToolCalls||300),enabledAsSubagent:e.enabledAsSubagent,model:e.modelMode===`fixed`?x(e.fixedModelValue):{mode:`inherit`},thinkingLevel:e.thinkingLevel}}function w(e){return!!(e.name.trim()&&e.label.trim()&&e.allowedTools.length>0)}async function T(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 E(){let[e,a]=(0,h.useState)([]),[c,l]=(0,h.useState)([]),[E,D]=(0,h.useState)(!1),[O,k]=(0,h.useState)(null),[A,j]=(0,h.useState)(()=>C()),[M,N]=(0,h.useState)(!1),[P,F]=(0,h.useState)(``),[I,ge]=(0,h.useState)(!1),[L,_e]=(0,h.useState)(),[R,ve]=(0,h.useState)([]),[ye,be]=(0,h.useState)(`off`),[z,B]=(0,h.useState)(``),[V,H]=(0,h.useState)(null),[U,W]=(0,h.useState)(null);async function xe(){let[e,t]=await Promise.all([T(`/api/agent-profiles`),T(`/api/agent-profiles/available-tools`)]);a(e.agents),l(t.tools)}(0,h.useEffect)(()=>{let e=!1;async function t(){try{let[t,n]=await Promise.all([T(`/api/agent-profiles`),T(`/api/agent-profiles/available-tools`)]);if(e)return;a(t.agents),l(n.tools)}catch(t){e||B(t instanceof Error?t.message:f(`requestFailed`))}}return t(),()=>{e=!0}},[]),(0,h.useEffect)(()=>{let e=!1;async function t(){try{let t=await re(),[n,r]=await Promise.all([ie(t),te().catch(()=>[])]),i=r.length?r:n;ve(i);let a=i.filter(m),o=await ae(t),s=(o.model&&m(o.model)?a.find(e=>le(e,o.model)):void 0)??a[0];if(e)return;_e(s),be(o.thinkingLevel??ne(s))}catch{}}return t(),()=>{e=!0}},[]),(0,h.useEffect)(()=>{if(!V)return;let e=()=>{H(null),W(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)}},[V]);let G=(0,h.useMemo)(()=>e.find(e=>e.id===O)??null,[e,O]),K=(0,h.useMemo)(()=>e.find(e=>e.id===V)??null,[e,V]),q=!!G?.readonly,J=!!(G?.readonly&&!G?.builtin),Y=(0,h.useMemo)(()=>{let e=x(A.fixedModelValue);if(e.mode!==`fixed`)return;let t=S(e);return R.find(e=>b(e)===t)??(`provider`in e?R.find(t=>ue(t,e)):void 0)},[A.fixedModelValue,R]),Se=(0,h.useMemo)(()=>R.filter(m),[R]),Ce=(0,h.useMemo)(()=>de(Se,Y),[Se,Y]),we=Y?b(Y):A.fixedModelValue,X=A.modelMode===`fixed`&&!!Y&&Y?.reasoning!==!0;function Te(e,t){if(e.stopPropagation(),V===t){H(null),W(null);return}let n=e.currentTarget.getBoundingClientRect(),r=Math.max(y,Math.min(n.right-_,window.innerWidth-_-y)),i=n.bottom+pe,a=n.top-pe-v;W({left:r,top:i+v<=window.innerHeight-y?i:Math.max(y,a)}),H(t)}function Z(e,t){j(n=>({...n,[e]:t}))}function Ee(e){ce(Y??null,Ce,e=>Z(`fixedModelValue`,e?b(e):``),{anchor:e.currentTarget,noneLabel:f(`agentModelInherit`)})}function De(e){j(t=>({...t,allowedTools:t.allowedTools.includes(e)?t.allowedTools.filter(t=>t!==e):[...t.allowedTools,e]}))}function Oe(){k(null),j(C()),F(``),B(``),D(!0)}function Q(e){k(e.id),j(me(e)),F(``),B(``),D(!0)}function $(){M||I||(D(!1),k(null),j(C()),F(``))}async function ke(){let e=P.trim();if(!e){B(f(`aiFillAgentInputRequired`));return}if(!L){B(f(`aiFillAgentNoModel`));return}ge(!0),B(``);try{let t=await T(`/api/agent-profiles/ai-fill`,{method:`POST`,body:JSON.stringify({instruction:e,modelRef:u(L),model:L,thinkingLevel:ye})});j(e=>({...e,name:t.agent.name,label:t.agent.label,description:t.agent.description,systemPrompt:t.agent.systemPrompt}))}catch(e){B(e instanceof Error?e.message:f(`aiFillAgentFailed`))}finally{ge(!1)}}async function Ae(){if(w(A)){N(!0),B(``);try{let e=G?.builtin?{model:A.modelMode===`fixed`?x(A.fixedModelValue):{mode:`inherit`}}:he({...A,thinkingLevel:X?`off`:A.thinkingLevel});O?await T(`/api/agent-profiles/${encodeURIComponent(O)}`,{method:`PATCH`,body:JSON.stringify(e)}):await T(`/api/agent-profiles`,{method:`POST`,body:JSON.stringify(e)}),$(),await xe()}catch(e){B(e instanceof Error?e.message:f(`requestFailed`))}finally{N(!1)}}}async function je(e){if(e.builtin||e.readonly)return;let t=!e.enabledAsSubagent,n=e.enabledAsSubagent;a(n=>n.map(n=>n.id===e.id?{...n,enabledAsSubagent:t}:n)),H(null);try{await T(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`PATCH`,body:JSON.stringify({enabledAsSubagent:t})})}catch(t){a(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:n}:t)),B(t instanceof Error?t.message:f(`requestFailed`))}}async function Me(e){if(!(e.builtin||e.readonly)&&await se({description:f(`confirmDeleteAgent`),confirmLabel:f(`confirmDelete`),cancelLabel:f(`cancel`),variant:`destructive`})){B(``);try{await T(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await xe()}catch(e){B(e instanceof Error?e.message:f(`requestFailed`))}}}return E?(0,g.jsxs)(`div`,{className:`quickforge-settings-stack`,children:[(0,g.jsx)(`div`,{className:`quickforge-settings-heading`,children:(0,g.jsxs)(`h3`,{className:`quickforge-settings-title`,children:[G?.builtin?f(`builtinAgentModelSettings`):f(G?`editAgent`:`createAgent`),(0,g.jsx)(oe,{label:G?.builtin?f(`builtinAgentModelOnly`):G?.readonly?f(`readonlyAgentDescription`):f(`agentsDescription`)})]})}),(0,g.jsxs)(`section`,{className:`quickforge-settings-section`,"aria-label":f(G?`editAgent`:`createAgent`),children:[(0,g.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,g.jsxs)(`button`,{className:`quickforge-settings-button quickforge-settings-button-secondary`,type:`button`,onClick:$,disabled:M||I,children:[(0,g.jsx)(i,{className:`mr-2 size-4`}),f(`back`)]}),(0,g.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,g.jsx)(`div`,{className:`quickforge-settings-row-title`,children:G?.builtin?f(`builtinAgentModelSettings`):f(G?`editAgent`:`createAgent`)}),G?.builtin?(0,g.jsx)(`div`,{className:`quickforge-settings-row-description`,children:f(`builtinAgentModelOnly`)}):G?.readonly?(0,g.jsx)(`div`,{className:`quickforge-settings-row-description`,children:f(`readonlyAgentDescription`)}):null]})]}),(0,g.jsx)(`div`,{className:`px-5 py-4`,children:(0,g.jsxs)(`div`,{className:`space-y-4`,children:[(0,g.jsxs)(`div`,{className:`rounded-2xl border border-border bg-muted/20 p-3`,children:[(0,g.jsxs)(`div`,{className:`mb-2 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,g.jsx)(ee,{className:`size-4 text-primary`}),f(`aiFillAgent`),(0,g.jsx)(oe,{label:f(`aiFillAgentDescription`)})]}),(0,g.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:P,disabled:q||I,onChange:e=>F(e.target.value),placeholder:f(`aiFillAgentPlaceholder`)}),(0,g.jsx)(`div`,{className:`mt-2 flex justify-end`,children:(0,g.jsxs)(p,{variant:`outline`,size:`sm`,onClick:()=>void ke(),disabled:q||I||!P.trim(),children:[(0,g.jsx)(ee,{className:`mr-1 size-3.5`}),f(I?`aiFillAgentLoading`:`aiFillAgent`)]})})]}),(0,g.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentName`),(0,g.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:A.name,disabled:q,onChange:e=>Z(`name`,e.target.value),placeholder:`reviewer`})]}),(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentLabel`),(0,g.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:A.label,disabled:q,onChange:e=>Z(`label`,e.target.value),placeholder:f(`agentLabelPlaceholder`)})]})]}),G?(0,g.jsxs)(`div`,{className:`rounded-xl border border-border bg-muted/20 px-3 py-2 text-sm`,children:[(0,g.jsx)(`div`,{className:`text-xs font-medium text-muted-foreground`,children:f(`agentSourcePath`)}),(0,g.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?f(`builtinAgent`):`-`})]}):null,(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentDescription`),(0,g.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:A.description,disabled:q,onChange:e=>Z(`description`,e.target.value)})]}),(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentSystemPrompt`),(0,g.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:A.systemPrompt,disabled:q,onChange:e=>Z(`systemPrompt`,e.target.value)})]}),(0,g.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentModelMode`),(0,g.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:A.modelMode,disabled:J,onChange:e=>Z(`modelMode`,e.target.value),children:[(0,g.jsx)(`option`,{value:`inherit`,children:f(`agentModelInherit`)}),(0,g.jsx)(`option`,{value:`fixed`,children:f(`agentModelFixed`)})]})]}),(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentFixedModel`),(0,g.jsxs)(`select`,{className:`quickforge-model-select-desktop 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:we,disabled:J||A.modelMode!==`fixed`,onChange:e=>Z(`fixedModelValue`,e.target.value),children:[(0,g.jsx)(`option`,{value:``,children:f(`agentModelInherit`)}),Ce.map(e=>(0,g.jsx)(`option`,{value:b(e),children:d(e)},b(e)))]}),(0,g.jsxs)(`button`,{type:`button`,className:`quickforge-model-select-mobile mt-1 flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 text-sm text-left outline-none focus:border-ring disabled:opacity-60`,disabled:J||A.modelMode!==`fixed`,onClick:Ee,children:[(0,g.jsx)(`span`,{className:`truncate`,children:Y?d(Y):f(`agentModelInherit`)}),(0,g.jsx)(n,{className:`ml-2 size-4 shrink-0 text-muted-foreground`})]})]})]}),(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`agentThinkingLevel`),(0,g.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&&A.thinkingLevel!==`inherit`?`off`:A.thinkingLevel,disabled:q||X,onChange:e=>Z(`thinkingLevel`,e.target.value),children:[(0,g.jsx)(`option`,{value:`inherit`,children:f(`agentThinkingInherit`)}),(0,g.jsx)(`option`,{value:`off`,children:f(`thinkingOff`)}),(0,g.jsx)(`option`,{value:`low`,children:f(`thinkingLow`)}),(0,g.jsx)(`option`,{value:`medium`,children:f(`thinkingMedium`)}),(0,g.jsx)(`option`,{value:`high`,children:f(`thinkingHigh`)}),(0,g.jsx)(`option`,{value:`xhigh`,children:f(`thinkingXHigh`)})]}),(0,g.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:f(X?`agentThinkingUnsupported`:`agentThinkingDescription`)})]}),(0,g.jsxs)(`div`,{children:[(0,g.jsx)(`div`,{className:`mb-2 text-sm font-medium text-foreground`,children:f(`allowedTools`)}),(0,g.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2`,children:c.map(e=>(0,g.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,g.jsx)(`input`,{type:`checkbox`,className:`mt-1`,disabled:q,checked:A.allowedTools.includes(e.name),onChange:()=>De(e.name)}),(0,g.jsxs)(`span`,{children:[(0,g.jsx)(`span`,{className:`font-medium text-foreground`,children:e.label}),(0,g.jsx)(`span`,{className:`ml-2 font-mono text-xs text-muted-foreground`,children:e.name}),e.riskLevel===`dangerous`?(0,g.jsx)(`span`,{className:`ml-2 rounded-full bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700`,children:f(`highRiskTool`)}):null,(0,g.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:e.description})]})]},e.name))})]}),(0,g.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`maxRuntimeMs`),(0,g.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:A.maxRuntimeMs,disabled:q,onChange:e=>Z(`maxRuntimeMs`,e.target.value)})]}),(0,g.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[f(`maxToolCalls`),(0,g.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:A.maxToolCalls,disabled:q,onChange:e=>Z(`maxToolCalls`,e.target.value)})]})]}),(0,g.jsxs)(`label`,{className:`flex items-center gap-2 text-sm text-foreground`,children:[(0,g.jsx)(`input`,{type:`checkbox`,checked:A.enabledAsSubagent,disabled:q,onChange:e=>Z(`enabledAsSubagent`,e.target.checked)}),f(`enabledAsSubagent`)]}),z?(0,g.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:z}):null]})}),(0,g.jsxs)(`div`,{className:`quickforge-settings-divider flex justify-end gap-2 px-5 py-4`,children:[(0,g.jsx)(p,{variant:`outline`,onClick:$,disabled:M||I,children:f(`cancel`)}),(0,g.jsx)(p,{onClick:Ae,disabled:M||I||J||!G?.builtin&&!w(A)||A.modelMode===`fixed`&&!A.fixedModelValue,children:f(`save`)})]})]})]}):(0,g.jsxs)(`div`,{className:`quickforge-settings-stack`,children:[(0,g.jsxs)(`section`,{className:`quickforge-settings-section`,"aria-label":f(`agentsTab`),children:[(0,g.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,g.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,g.jsxs)(`div`,{className:`quickforge-settings-row-title`,children:[(0,g.jsx)(r,{className:`size-4 text-primary`}),f(`agentsTab`)]}),(0,g.jsx)(`div`,{className:`quickforge-settings-row-description`,children:f(`agentsDescription`)})]}),(0,g.jsx)(`button`,{className:`quickforge-settings-button quickforge-settings-button-primary`,type:`button`,onClick:Oe,children:f(`createAgent`)})]}),z?(0,g.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:z}):null,e.length===0?(0,g.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:f(`loading`)}):e.map(e=>(0,g.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,g.jsx)(`div`,{className:`quickforge-settings-list-item-main quickforge-agent-profile-row-main`,children:(0,g.jsxs)(`div`,{className:`quickforge-agent-profile-summary`,children:[(0,g.jsx)(`span`,{className:`quickforge-agent-profile-label`,title:e.label,children:e.label}),e.description?(0,g.jsx)(`span`,{className:`quickforge-agent-profile-description`,title:e.description,children:e.description}):null]})}),(0,g.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,g.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":e.builtin||e.readonly?`true`:`false`,title:e.enabledAsSubagent?f(`disableAsSubagent`):f(`enableAsSubagent`),children:[(0,g.jsx)(`input`,{type:`checkbox`,checked:e.enabledAsSubagent,disabled:e.builtin||e.readonly,onChange:()=>void je(e)}),(0,g.jsx)(`span`,{"aria-hidden":`true`})]}),(0,g.jsx)(`button`,{className:`quickforge-settings-icon-action`,type:`button`,onClick:t=>Te(t,e.id),title:f(`moreActions`),"aria-label":f(`moreActions`),"aria-haspopup":`menu`,"aria-expanded":V===e.id,children:(0,g.jsx)(o,{className:`size-4`})})]})]},e.id))]}),K&&U?(0,fe.createPortal)((0,g.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:U.left,top:U.top},role:`menu`,"aria-label":f(`moreActions`),onClick:e=>e.stopPropagation(),children:[(0,g.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:()=>{H(null),W(null),Q(K)},children:[(0,g.jsx)(t,{className:`size-3.5`}),K.builtin?f(`builtinAgentModelSettings`):f(`editTask`)]}),(0,g.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:()=>{H(null),W(null),Me(K)},children:[(0,g.jsx)(s,{className:`size-3.5`}),f(`delete`)]})]}),document.body):null]})}export{E as AgentProfilesPage};