@shawnstack/quickforge 1.6.2 → 1.6.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 (45) hide show
  1. package/README.md +452 -450
  2. package/dist/assets/AgentProfilesPage-XJdWmkQb.js +1 -0
  3. package/dist/assets/{ChatPanelHost-CnQoAfxW.js → ChatPanelHost-BrUHIzBw.js} +68 -68
  4. package/dist/assets/PluginsPage-DDxjlhiK.js +1 -0
  5. package/dist/assets/ScheduledTasksPage-Bw-pSpbv.js +2 -0
  6. package/dist/assets/SettingsWorkspacePage-B_7o3noo.js +1412 -0
  7. package/dist/assets/SharedConversationPage-B5DPQla3.js +1 -0
  8. package/dist/assets/TerminalDock-CVObjSM6.js +2 -0
  9. package/dist/assets/WorkspaceInspector-Dk5887WR.js +3 -0
  10. package/dist/assets/WorkspaceReaderDialog-D-_BBvud.js +1 -0
  11. package/dist/assets/diff-line-counts-DagL0jQr.js +10 -0
  12. package/dist/assets/icons-DHoaB5uq.js +1 -0
  13. package/dist/assets/index-C77QCHE9.css +3 -0
  14. package/dist/assets/index-DeUvsbzW.js +63 -0
  15. package/dist/assets/mcp-servers-dialog-CJ4HKWVZ.js +5 -0
  16. package/dist/assets/{monaco-C5Lhy6-h.js → monaco-FCb94ino.js} +1 -1
  17. package/dist/assets/{react-vendor--m65aOgw.js → react-vendor-BR9MG2D-.js} +1 -1
  18. package/dist/assets/skills-dialog-CmCLl7K_.js +1 -0
  19. package/dist/assets/{useAppTheme-ChXh3B2c.js → useAppTheme-BwZ5OEf3.js} +1 -1
  20. package/dist/assets/vscode-C_7wk1WI.svg +41 -0
  21. package/dist/index.html +6 -6
  22. package/package.json +1 -1
  23. package/server/agent-manager.mjs +3 -1
  24. package/server/index.mjs +5 -4
  25. package/server/routes/backup.mjs +80 -6
  26. package/server/routes/project.mjs +27 -1
  27. package/server/routes/system.mjs +5 -0
  28. package/server/routes/workspace.mjs +351 -0
  29. package/server/storage.mjs +6 -5
  30. package/server/utils/package-update.mjs +46 -0
  31. package/server/utils/platform.mjs +108 -0
  32. package/dist/assets/AgentProfilesPage-wpt1cJV0.js +0 -1
  33. package/dist/assets/PluginsPage-0wwQydcP.js +0 -1
  34. package/dist/assets/ScheduledTasksPage-qGHyGkjw.js +0 -2
  35. package/dist/assets/SettingsWorkspacePage-CeYg_VJl.js +0 -1320
  36. package/dist/assets/SharedConversationPage-D97o9Ycp.js +0 -1
  37. package/dist/assets/TerminalDock-D4zLoahb.js +0 -2
  38. package/dist/assets/WorkspaceInspector-Bn-am8g6.js +0 -3
  39. package/dist/assets/WorkspaceReaderDialog-B2WEMVuF.js +0 -1
  40. package/dist/assets/diff-line-counts-B60LmPZX.js +0 -10
  41. package/dist/assets/icons-BlAm_pUr.js +0 -1
  42. package/dist/assets/index-C3bhHGO4.css +0 -3
  43. package/dist/assets/index-C_DXqhW8.js +0 -69
  44. package/dist/assets/mcp-servers-dialog-DEyq3gmP.js +0 -20
  45. package/dist/assets/skills-dialog-CZ3CuVtQ.js +0 -1
@@ -1,8 +1,11 @@
1
1
  import { promises as fs } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawn } from 'node:child_process'
4
+ import { streamSimple } from '@earendil-works/pi-ai'
4
5
  import { sendJson, readJsonBody } from '../utils/response.mjs'
5
6
  import { projectContextFromId } from '../project-config.mjs'
7
+ import { readStore } from '../storage.mjs'
8
+ import { logger } from '../utils/logger.mjs'
6
9
  import {
7
10
  assertSafeWorkspacePath,
8
11
  resolveWorkspacePath,
@@ -145,6 +148,97 @@ async function currentGitBranch(workspaceRoot) {
145
148
  return commit ? `HEAD ${commit}` : undefined
146
149
  }
147
150
 
151
+ async function assertValidBranchName(workspaceRoot, branch) {
152
+ const value = typeof branch === 'string' ? branch.trim() : ''
153
+ if (!value || value.length > 240 || /[\0\r\n]/.test(value)) {
154
+ const error = new Error('Invalid branch name')
155
+ error.statusCode = 400
156
+ throw error
157
+ }
158
+ const result = await git(['check-ref-format', '--branch', value], workspaceRoot, { allowFailure: true })
159
+ if (result.code !== 0) {
160
+ const error = new Error('Invalid branch name')
161
+ error.statusCode = 400
162
+ throw error
163
+ }
164
+ return value
165
+ }
166
+
167
+ function branchSortKey(branch, current) {
168
+ return [branch.name === current ? '0' : '1', branch.remote ? '1' : '0', branch.name.toLowerCase()].join(':')
169
+ }
170
+
171
+ async function listGitBranches(context) {
172
+ if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, branches: [] }
173
+ const current = await currentGitBranch(context.workspaceRoot)
174
+ const result = await git([
175
+ 'for-each-ref',
176
+ '--format=%(refname)%1f%(refname:short)%1f%(objectname:short)%1f%(committerdate:iso8601-strict)%1f%(upstream:short)',
177
+ 'refs/heads',
178
+ 'refs/remotes',
179
+ ], context.workspaceRoot)
180
+ const branches = result.stdout.toString('utf8').split('\n')
181
+ .map((line) => line.trim())
182
+ .filter(Boolean)
183
+ .map((line) => {
184
+ const [refname = '', name = '', commit = '', lastCommitAt = '', upstream = ''] = line.split('\x1f')
185
+ const remote = refname.startsWith('refs/remotes/')
186
+ return {
187
+ name,
188
+ current: name === current,
189
+ remote,
190
+ upstream: upstream || undefined,
191
+ commit: commit || undefined,
192
+ lastCommitAt: lastCommitAt || undefined,
193
+ }
194
+ })
195
+ .filter((branch) => branch.name && !branch.name.endsWith('/HEAD'))
196
+ .sort((left, right) => branchSortKey(left, current).localeCompare(branchSortKey(right, current)))
197
+ return { isGitRepository: true, current, branches }
198
+ }
199
+
200
+ function parseGitDecorations(raw) {
201
+ if (!raw) return []
202
+ return raw.split(', ')
203
+ .map((entry) => entry.trim())
204
+ .filter(Boolean)
205
+ .map((entry) => {
206
+ if (entry === 'HEAD') return { name: 'HEAD', type: 'head' }
207
+ if (entry.startsWith('HEAD -> ')) return { name: entry.slice('HEAD -> '.length), type: 'branch' }
208
+ if (entry.startsWith('tag: ')) return { name: entry.slice('tag: '.length), type: 'tag' }
209
+ if (entry.includes('/')) return { name: entry, type: 'remote' }
210
+ return { name: entry, type: 'branch' }
211
+ })
212
+ }
213
+
214
+ async function listGitLog(context) {
215
+ if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, commits: [] }
216
+ const result = await git([
217
+ 'log',
218
+ '--all',
219
+ '--date=iso-strict',
220
+ '--max-count=200',
221
+ '--format=%H%x1f%h%x1f%P%x1f%an%x1f%aI%x1f%D%x1f%s%x1e',
222
+ ], context.workspaceRoot, { allowFailure: true })
223
+ if (result.code !== 0) return { isGitRepository: true, commits: [] }
224
+ const commits = result.stdout.toString('utf8').split('\x1e')
225
+ .map((record) => record.trim())
226
+ .filter(Boolean)
227
+ .map((record) => {
228
+ const [hash = '', shortHash = '', parents = '', author = '', date = '', decorations = '', subject = ''] = record.split('\x1f')
229
+ return {
230
+ hash,
231
+ shortHash,
232
+ parents: parents ? parents.split(' ').filter(Boolean) : [],
233
+ author,
234
+ date,
235
+ subject,
236
+ decorations: parseGitDecorations(decorations),
237
+ }
238
+ })
239
+ return { isGitRepository: true, commits }
240
+ }
241
+
148
242
  function countGitStatus(files) {
149
243
  return files.reduce((counts, file) => {
150
244
  if (file.conflict) counts.conflicts += 1
@@ -231,6 +325,150 @@ async function listGitStatus(context) {
231
325
  }
232
326
  }
233
327
 
328
+ async function assertGitRepository(context) {
329
+ if (await isGitRepository(context.workspaceRoot)) return
330
+ const error = new Error('Not a Git repository')
331
+ error.statusCode = 400
332
+ throw error
333
+ }
334
+
335
+ function normalizeCommitMessage(value) {
336
+ const raw = String(value || '').trim()
337
+ if (!raw) {
338
+ const error = new Error('Commit message is required')
339
+ error.statusCode = 400
340
+ throw error
341
+ }
342
+ return raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n').slice(0, 4000)
343
+ }
344
+
345
+ async function hasStagedChanges(context) {
346
+ const result = await git(['diff', '--cached', '--quiet'], context.workspaceRoot, { allowFailure: true })
347
+ return result.code === 1
348
+ }
349
+
350
+ async function commitGitChanges(context, message, includeUnstaged) {
351
+ await assertGitRepository(context)
352
+ const commitMessage = normalizeCommitMessage(message)
353
+ if (includeUnstaged) await git(['add', '-A'], context.workspaceRoot)
354
+ if (!(await hasStagedChanges(context))) {
355
+ const error = new Error('No staged changes to commit')
356
+ error.statusCode = 400
357
+ throw error
358
+ }
359
+ await git(['commit', '-m', commitMessage], context.workspaceRoot)
360
+ return listGitStatus(context)
361
+ }
362
+
363
+ async function pushGitBranch(context) {
364
+ await assertGitRepository(context)
365
+ await git(['push'], context.workspaceRoot)
366
+ return listGitStatus(context)
367
+ }
368
+
369
+ async function getApiKey(provider) {
370
+ try {
371
+ const keys = await readStore('provider-keys')
372
+ return keys?.[provider] || undefined
373
+ } catch {
374
+ return undefined
375
+ }
376
+ }
377
+
378
+ function trimForPrompt(text, max = 14000) {
379
+ const raw = String(text || '').trim()
380
+ if (raw.length <= max) return raw
381
+ return `${raw.slice(0, max)}\n\n[Diff truncated]`
382
+ }
383
+
384
+ function normalizeAiCommitMessage(text) {
385
+ const raw = String(text || '').trim()
386
+ .replace(/^```(?:text)?/i, '')
387
+ .replace(/```$/i, '')
388
+ .trim()
389
+ const lines = raw.split('\n').map((line) => line.trimEnd())
390
+ while (lines.length && !lines[0].trim()) lines.shift()
391
+ while (lines.length && !lines[lines.length - 1].trim()) lines.pop()
392
+ const message = lines.join('\n').trim().slice(0, 2000)
393
+ if (!message) {
394
+ const error = new Error('AI did not generate a commit message')
395
+ error.statusCode = 502
396
+ throw error
397
+ }
398
+ return message
399
+ }
400
+
401
+ async function generateGitCommitMessage(context, model, thinkingLevel = 'off') {
402
+ await assertGitRepository(context)
403
+ if (!model) {
404
+ const error = new Error('Please configure a default model first')
405
+ error.statusCode = 400
406
+ throw error
407
+ }
408
+
409
+ const status = await listGitStatus(context)
410
+ if (!status.counts?.total) {
411
+ const error = new Error('No Git changes to summarize')
412
+ error.statusCode = 400
413
+ throw error
414
+ }
415
+
416
+ const cachedStat = (await git(['diff', '--cached', '--stat'], context.workspaceRoot, { allowFailure: true })).stdout.toString('utf8')
417
+ const cachedDiff = (await git(['diff', '--cached'], context.workspaceRoot, { allowFailure: true })).stdout.toString('utf8')
418
+ const worktreeStat = (await git(['diff', '--stat'], context.workspaceRoot, { allowFailure: true })).stdout.toString('utf8')
419
+ const worktreeDiff = (await git(['diff'], context.workspaceRoot, { allowFailure: true })).stdout.toString('utf8')
420
+ const files = status.files.map((file) => `- ${file.status}${file.staged ? ' staged' : ''}${file.unstaged ? ' unstaged' : ''}: ${file.oldPath ? `${file.oldPath} -> ` : ''}${file.path}`).join('\n')
421
+ const systemPrompt = `You generate Git commit messages.
422
+ Return only the commit message, no Markdown, no explanation.
423
+ Use Conventional Commit style when possible, for example: feat: add git tools summary.
424
+ Keep the subject under 72 characters. Add a short body only if it is useful.`
425
+ const userPrompt = `Current branch: ${status.branch || 'unknown'}
426
+
427
+ Changed files:
428
+ ${files}
429
+
430
+ Staged diff stat:
431
+ ${cachedStat || '(none)'}
432
+
433
+ Staged diff:
434
+ ${trimForPrompt(cachedDiff)}
435
+
436
+ Unstaged diff stat:
437
+ ${worktreeStat || '(none)'}
438
+
439
+ Unstaged diff:
440
+ ${trimForPrompt(worktreeDiff)}`
441
+
442
+ try {
443
+ const stream = streamSimple(
444
+ model,
445
+ {
446
+ systemPrompt,
447
+ messages: [{ role: 'user', content: userPrompt, timestamp: Date.now() }],
448
+ tools: [],
449
+ },
450
+ {
451
+ apiKey: await getApiKey(model.provider),
452
+ maxTokens: 500,
453
+ temperature: 0,
454
+ reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
455
+ maxRetryDelayMs: 60000,
456
+ },
457
+ )
458
+ const message = await stream.result()
459
+ const content = Array.isArray(message.content)
460
+ ? message.content.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('\n')
461
+ : ''
462
+ return normalizeAiCommitMessage(content)
463
+ } catch (error) {
464
+ if (error?.statusCode) throw error
465
+ logger.warn('AI commit message generation failed:', error?.message || error)
466
+ const wrapped = new Error(`AI generation failed: ${error?.message || 'check model configuration and API key'}`)
467
+ wrapped.statusCode = 502
468
+ throw wrapped
469
+ }
470
+ }
471
+
234
472
  async function readGitFile(workspaceRoot, ref, relativePath) {
235
473
  const result = await git(['show', `${ref}:${relativePath}`], workspaceRoot, { allowFailure: true })
236
474
  return result.code === 0 ? result.stdout.toString('utf8') : ''
@@ -407,6 +645,54 @@ async function handleGitStatus(req, res, url) {
407
645
  sendJson(res, 200, await listGitStatus(context))
408
646
  }
409
647
 
648
+ async function handleGitBranches(req, res, url) {
649
+ const context = await projectContextFromUrl(url)
650
+ sendJson(res, 200, await listGitBranches(context))
651
+ }
652
+
653
+ async function handleGitLog(req, res, url) {
654
+ const context = await projectContextFromUrl(url)
655
+ sendJson(res, 200, await listGitLog(context))
656
+ }
657
+
658
+ async function handleGitCheckout(req, res) {
659
+ const body = await readJsonBody(req, 16 * 1024)
660
+ const projectId = typeof body?.projectId === 'string' ? body.projectId : ''
661
+ if (!projectId) {
662
+ const error = new Error('projectId is required')
663
+ error.statusCode = 400
664
+ throw error
665
+ }
666
+ const context = await projectContextFromId(projectId)
667
+ if (!(await isGitRepository(context.workspaceRoot))) {
668
+ const error = new Error('This project is not a Git repository')
669
+ error.statusCode = 400
670
+ throw error
671
+ }
672
+ const branch = await assertValidBranchName(context.workspaceRoot, body?.branch)
673
+ await git(['checkout', branch], context.workspaceRoot)
674
+ sendJson(res, 200, await listGitStatus(context))
675
+ }
676
+
677
+ async function handleGitCreateBranch(req, res) {
678
+ const body = await readJsonBody(req, 16 * 1024)
679
+ const projectId = typeof body?.projectId === 'string' ? body.projectId : ''
680
+ if (!projectId) {
681
+ const error = new Error('projectId is required')
682
+ error.statusCode = 400
683
+ throw error
684
+ }
685
+ const context = await projectContextFromId(projectId)
686
+ if (!(await isGitRepository(context.workspaceRoot))) {
687
+ const error = new Error('This project is not a Git repository')
688
+ error.statusCode = 400
689
+ throw error
690
+ }
691
+ const branch = await assertValidBranchName(context.workspaceRoot, body?.branch)
692
+ await git(['checkout', '-b', branch], context.workspaceRoot)
693
+ sendJson(res, 200, await listGitStatus(context))
694
+ }
695
+
410
696
  async function handleGitFileDiff(req, res, url) {
411
697
  const context = await projectContextFromUrl(url)
412
698
  const relativePath = url.searchParams.get('path') || ''
@@ -453,6 +739,39 @@ async function handleGitFileDiff(req, res, url) {
453
739
  })
454
740
  }
455
741
 
742
+ async function contextFromGitBody(req) {
743
+ const body = await readJsonBody(req, 1024 * 1024)
744
+ const projectId = typeof body?.projectId === 'string' ? body.projectId : ''
745
+ if (!projectId) {
746
+ const error = new Error('projectId is required')
747
+ error.statusCode = 400
748
+ throw error
749
+ }
750
+ return { context: await projectContextFromId(projectId), body }
751
+ }
752
+
753
+ async function handleGitGenerateCommitMessage(req, res) {
754
+ const { context, body } = await contextFromGitBody(req)
755
+ const message = await generateGitCommitMessage(context, body?.model, body?.thinkingLevel)
756
+ sendJson(res, 200, { message })
757
+ }
758
+
759
+ async function handleGitCommit(req, res) {
760
+ const { context, body } = await contextFromGitBody(req)
761
+ sendJson(res, 200, await commitGitChanges(context, body?.message, Boolean(body?.includeUnstaged)))
762
+ }
763
+
764
+ async function handleGitPush(req, res) {
765
+ const { context } = await contextFromGitBody(req)
766
+ sendJson(res, 200, await pushGitBranch(context))
767
+ }
768
+
769
+ async function handleGitCommitAndPush(req, res) {
770
+ const { context, body } = await contextFromGitBody(req)
771
+ await commitGitChanges(context, body?.message, Boolean(body?.includeUnstaged))
772
+ sendJson(res, 200, await pushGitBranch(context))
773
+ }
774
+
456
775
  export async function handleWorkspaceApi(req, res, url) {
457
776
  if (req.method === 'GET' && url.pathname === '/api/workspace/tree') {
458
777
  await handleWorkspaceTree(req, res, url)
@@ -481,10 +800,42 @@ export async function handleGitApi(req, res, url) {
481
800
  await handleGitStatus(req, res, url)
482
801
  return
483
802
  }
803
+ if (req.method === 'GET' && url.pathname === '/api/git/branches') {
804
+ await handleGitBranches(req, res, url)
805
+ return
806
+ }
807
+ if (req.method === 'GET' && url.pathname === '/api/git/log') {
808
+ await handleGitLog(req, res, url)
809
+ return
810
+ }
811
+ if (req.method === 'POST' && url.pathname === '/api/git/checkout') {
812
+ await handleGitCheckout(req, res)
813
+ return
814
+ }
815
+ if (req.method === 'POST' && url.pathname === '/api/git/create-branch') {
816
+ await handleGitCreateBranch(req, res)
817
+ return
818
+ }
484
819
  if (req.method === 'GET' && url.pathname === '/api/git/file-diff') {
485
820
  await handleGitFileDiff(req, res, url)
486
821
  return
487
822
  }
823
+ if (req.method === 'POST' && url.pathname === '/api/git/generate-commit-message') {
824
+ await handleGitGenerateCommitMessage(req, res)
825
+ return
826
+ }
827
+ if (req.method === 'POST' && url.pathname === '/api/git/commit') {
828
+ await handleGitCommit(req, res)
829
+ return
830
+ }
831
+ if (req.method === 'POST' && url.pathname === '/api/git/push') {
832
+ await handleGitPush(req, res)
833
+ return
834
+ }
835
+ if (req.method === 'POST' && url.pathname === '/api/git/commit-and-push') {
836
+ await handleGitCommitAndPush(req, res)
837
+ return
838
+ }
488
839
 
489
840
  const error = new Error('Not found')
490
841
  error.statusCode = 404
@@ -469,10 +469,6 @@ async function readAllSessionValues() {
469
469
  return result
470
470
  }
471
471
 
472
- function sessionMetadataQueueName(bucket) {
473
- return bucket.scope === 'project' ? `sessions-metadata:${bucket.projectId}` : 'sessions-metadata:global'
474
- }
475
-
476
472
  function sameSessionBucket(left, right) {
477
473
  if (!left || !right) return false
478
474
  return left.scope === right.scope && (left.projectId || undefined) === (right.projectId || undefined)
@@ -852,7 +848,12 @@ export async function atomicUpdate(storeName, updateFn) {
852
848
  export async function atomicSessionMetadataUpdate(scope, projectId, updateFn) {
853
849
  const bucket = scope === 'project' ? { scope: 'project', projectId } : { scope: 'global' }
854
850
  const file = sessionStoreFile('sessions-metadata', bucket)
855
- return enqueueWrite(sessionMetadataQueueName(bucket), async () => {
851
+ // Scoped session-metadata writes MUST share the single 'sessions-metadata'
852
+ // write queue used by atomicUpdate('sessions-metadata'). Both paths
853
+ // read-modify-write the same physical files; separate queues let them run
854
+ // concurrently and clobber each other — e.g. persistSession (which rebuilds
855
+ // metadata without pinnedAt) racing with a pin update and dropping pinnedAt.
856
+ return enqueueWrite('sessions-metadata', async () => {
856
857
  await ensureStorage()
857
858
  const data = await readJsonFile(file, {})
858
859
  const previousData = { ...data }
@@ -2,6 +2,9 @@ import { spawn } from 'node:child_process'
2
2
  import { promises as fs } from 'node:fs'
3
3
  import path from 'node:path'
4
4
 
5
+ const QUICKFORGE_RELEASES_URL = 'https://github.com/shawnstack/quickforge/releases/latest'
6
+ const QUICKFORGE_LATEST_RELEASE_API_URL = 'https://api.github.com/repos/shawnstack/quickforge/releases/latest'
7
+
5
8
  function normalizeRepositoryUrl(value) {
6
9
  if (!value || typeof value !== 'string') return ''
7
10
  return value
@@ -122,11 +125,54 @@ export async function checkForUpdates(projectRoot) {
122
125
  const comparison = compareVersions(pkg.version, latestVersion)
123
126
  return {
124
127
  ...pkg,
128
+ channel: 'npm-runtime',
129
+ distribution: 'npm',
125
130
  currentVersion: pkg.version,
126
131
  latestVersion,
127
132
  updateAvailable: comparison < 0,
128
133
  localVersionIsNewer: comparison > 0,
129
134
  installCommand: `npm install -g ${pkg.name}@latest`,
135
+ releaseUrl: QUICKFORGE_RELEASES_URL,
136
+ }
137
+ }
138
+
139
+ export async function checkDesktopRelease(projectRoot) {
140
+ const pkg = await getPackageInfo(projectRoot)
141
+ const controller = new AbortController()
142
+ const timeout = setTimeout(() => controller.abort(), 5000)
143
+
144
+ try {
145
+ const response = await fetch(QUICKFORGE_LATEST_RELEASE_API_URL, {
146
+ headers: {
147
+ accept: 'application/vnd.github+json',
148
+ 'user-agent': `${pkg.name || 'quickforge'}-desktop-update-check`,
149
+ },
150
+ signal: controller.signal,
151
+ })
152
+
153
+ if (!response.ok) throw new Error(`GitHub releases returned HTTP ${response.status}`)
154
+
155
+ const release = await response.json()
156
+ const latestVersion = release?.tag_name || release?.name
157
+ if (!latestVersion || typeof latestVersion !== 'string') throw new Error('latest release version not found in GitHub response')
158
+
159
+ const comparison = compareVersions(pkg.version, latestVersion)
160
+ return {
161
+ ...pkg,
162
+ channel: 'desktop-app',
163
+ distribution: 'github-releases',
164
+ currentVersion: pkg.version,
165
+ latestVersion,
166
+ updateAvailable: comparison < 0,
167
+ localVersionIsNewer: comparison > 0,
168
+ releaseUrl: release?.html_url || QUICKFORGE_RELEASES_URL,
169
+ installable: false,
170
+ }
171
+ } catch (error) {
172
+ if (error.name === 'AbortError') throw new Error('request timeout', { cause: error })
173
+ throw error
174
+ } finally {
175
+ clearTimeout(timeout)
130
176
  }
131
177
  }
132
178
 
@@ -122,6 +122,15 @@ try {
122
122
  throw error
123
123
  }
124
124
 
125
+ async function findExistingFile(candidates) {
126
+ for (const candidate of candidates) {
127
+ if (!candidate) continue
128
+ const stat = await fs.stat(candidate).catch(() => null)
129
+ if (stat?.isFile()) return candidate
130
+ }
131
+ return undefined
132
+ }
133
+
125
134
  export async function openPathInFileManager(targetPath) {
126
135
  const resolved = path.resolve(String(targetPath || ''))
127
136
  const stat = await fs.stat(resolved).catch(() => null)
@@ -151,6 +160,105 @@ export async function openPathInFileManager(targetPath) {
151
160
  })
152
161
  }
153
162
 
163
+ export async function openPathInVSCode(targetPath) {
164
+ const resolved = path.resolve(String(targetPath || ''))
165
+ const stat = await fs.stat(resolved).catch(() => null)
166
+ if (!stat || !stat.isDirectory()) {
167
+ const error = new Error(`Directory does not exist: ${resolved}`)
168
+ error.statusCode = 400
169
+ throw error
170
+ }
171
+
172
+ let command = 'code'
173
+ let args = [resolved]
174
+ if (process.platform === 'darwin') {
175
+ command = 'open'
176
+ args = ['-a', 'Visual Studio Code', resolved]
177
+ } else if (process.platform === 'win32') {
178
+ const candidates = [
179
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'Microsoft VS Code', 'Code.exe') : undefined,
180
+ process.env.PROGRAMFILES ? path.join(process.env.PROGRAMFILES, 'Microsoft VS Code', 'Code.exe') : undefined,
181
+ process.env['PROGRAMFILES(X86)'] ? path.join(process.env['PROGRAMFILES(X86)'], 'Microsoft VS Code', 'Code.exe') : undefined,
182
+ ].filter(Boolean)
183
+ const codeExecutable = await findExistingFile(candidates)
184
+ if (codeExecutable) {
185
+ command = codeExecutable
186
+ args = [resolved]
187
+ } else {
188
+ command = 'cmd.exe'
189
+ args = ['/d', '/s', '/c', 'start', '""', '/b', 'code', resolved]
190
+ }
191
+ }
192
+
193
+ await new Promise((resolve, reject) => {
194
+ const child = spawn(command, args, {
195
+ detached: true,
196
+ stdio: 'ignore',
197
+ windowsHide: true,
198
+ shell: false,
199
+ })
200
+ child.once('error', (error) => {
201
+ error.statusCode = 500
202
+ reject(error)
203
+ })
204
+ child.once('spawn', () => {
205
+ child.unref()
206
+ resolve()
207
+ })
208
+ })
209
+ }
210
+
211
+ export async function openPathInIDEA(targetPath) {
212
+ const resolved = path.resolve(String(targetPath || ''))
213
+ const stat = await fs.stat(resolved).catch(() => null)
214
+ if (!stat || !stat.isDirectory()) {
215
+ const error = new Error(`Directory does not exist: ${resolved}`)
216
+ error.statusCode = 400
217
+ throw error
218
+ }
219
+
220
+ let command = 'idea'
221
+ let args = [resolved]
222
+ if (process.platform === 'darwin') {
223
+ command = 'open'
224
+ args = ['-a', 'IntelliJ IDEA', resolved]
225
+ } else if (process.platform === 'win32') {
226
+ const candidates = [
227
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'IntelliJ IDEA', 'bin', 'idea64.exe') : undefined,
228
+ process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Programs', 'IntelliJ IDEA', 'bin', 'idea.exe') : undefined,
229
+ process.env.PROGRAMFILES ? path.join(process.env.PROGRAMFILES, 'JetBrains', 'IntelliJ IDEA', 'bin', 'idea64.exe') : undefined,
230
+ process.env.PROGRAMFILES ? path.join(process.env.PROGRAMFILES, 'JetBrains', 'IntelliJ IDEA', 'bin', 'idea.exe') : undefined,
231
+ process.env['PROGRAMFILES(X86)'] ? path.join(process.env['PROGRAMFILES(X86)'], 'JetBrains', 'IntelliJ IDEA', 'bin', 'idea64.exe') : undefined,
232
+ process.env['PROGRAMFILES(X86)'] ? path.join(process.env['PROGRAMFILES(X86)'], 'JetBrains', 'IntelliJ IDEA', 'bin', 'idea.exe') : undefined,
233
+ ].filter(Boolean)
234
+ const ideaExecutable = await findExistingFile(candidates)
235
+ if (ideaExecutable) {
236
+ command = ideaExecutable
237
+ args = [resolved]
238
+ } else {
239
+ command = 'cmd.exe'
240
+ args = ['/d', '/s', '/c', 'start', '""', '/b', 'idea', resolved]
241
+ }
242
+ }
243
+
244
+ await new Promise((resolve, reject) => {
245
+ const child = spawn(command, args, {
246
+ detached: true,
247
+ stdio: 'ignore',
248
+ windowsHide: true,
249
+ shell: false,
250
+ })
251
+ child.once('error', (error) => {
252
+ error.statusCode = 500
253
+ reject(error)
254
+ })
255
+ child.once('spawn', () => {
256
+ child.unref()
257
+ resolve()
258
+ })
259
+ })
260
+ }
261
+
154
262
  export function openBrowser(url) {
155
263
  if (process.env.QUICKFORGE_NO_OPEN === '1') return
156
264
 
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{Dt as t,Mt as n,T as r,a as i,at as a,c as o}from"./icons-BlAm_pUr.js";import{n as s}from"./react-vendor--m65aOgw.js";import{$ as c,G as l,H as u,K as d,M as f,Q as p,U as m,W as h,et as g,it as _}from"./index-C_DXqhW8.js";var v=e(n(),1),y=s();function b(){return{name:``,label:``,description:``,systemPrompt:``,allowedTools:[`read_file`,`grep_files`],maxRuntimeMs:`1800000`,maxToolCalls:`300`,enabledAsSubagent:!0}}function x(e){return{name:e.name,label:e.label,description:e.description??``,systemPrompt:e.systemPrompt??``,allowedTools:e.allowedTools??[],maxRuntimeMs:String(e.maxRuntimeMs??18e5),maxToolCalls:String(e.maxToolCalls??300),enabledAsSubagent:e.enabledAsSubagent}}function S(e){return{name:e.name.trim().toLowerCase(),label:e.label.trim(),description:e.description.trim(),systemPrompt:e.systemPrompt.trim(),allowedTools:e.allowedTools,maxRuntimeMs:Number(e.maxRuntimeMs||18e5),maxToolCalls:Number(e.maxToolCalls||300),enabledAsSubagent:e.enabledAsSubagent}}function C(e){return!!(e.name.trim()&&e.label.trim()&&e.allowedTools.length>0)}async function w(e,t){let n=await fetch(e,{...t,headers:{"content-type":`application/json`,...t?.headers}}),r=await n.json().catch(()=>null);if(!n.ok)throw Error(r?.error||`请求失败`);return r}function T(){let[e,n]=(0,v.useState)([]),[s,T]=(0,v.useState)([]),[E,D]=(0,v.useState)(!1),[O,k]=(0,v.useState)(null),[A,j]=(0,v.useState)(()=>b()),[M,N]=(0,v.useState)(!1),[P,F]=(0,v.useState)(``),[I,L]=(0,v.useState)(!1),[R,z]=(0,v.useState)(),[B,V]=(0,v.useState)(`off`),[H,U]=(0,v.useState)(``),[W,G]=(0,v.useState)(null);async function K(){let[e,t]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);n(e.agents),T(t.tools)}(0,v.useEffect)(()=>{let e=!1;async function t(){try{let[t,r]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);if(e)return;n(t.agents),T(r.tools)}catch(t){e||U(t instanceof Error?t.message:_(`requestFailed`))}}return t(),()=>{e=!0}},[]),(0,v.useEffect)(()=>{let e=!1;async function t(){try{let t=await h(),n=await m(t),r=await l(t),i=r.model??await d(t)??n[0];if(e)return;z(i),V(r.thinkingLevel??u(i))}catch{}}return t(),()=>{e=!0}},[]),(0,v.useEffect)(()=>{if(!W)return;let e=()=>G(null);return window.addEventListener(`click`,e),window.addEventListener(`blur`,e),()=>{window.removeEventListener(`click`,e),window.removeEventListener(`blur`,e)}},[W]);let q=(0,v.useMemo)(()=>e.find(e=>e.id===O)??null,[e,O]);function J(e,t){j(n=>({...n,[e]:t}))}function Y(e){j(t=>({...t,allowedTools:t.allowedTools.includes(e)?t.allowedTools.filter(t=>t!==e):[...t.allowedTools,e]}))}function X(){k(null),j(b()),F(``),U(``),D(!0)}function Z(e){k(e.id),j(x(e)),F(``),U(``),D(!0)}function Q(){M||I||(D(!1),k(null),j(b()),F(``))}async function $(){let e=P.trim();if(!e){U(_(`aiFillAgentInputRequired`));return}if(!R){U(_(`aiFillAgentNoModel`));return}L(!0),U(``);try{let t=await w(`/api/agent-profiles/ai-fill`,{method:`POST`,body:JSON.stringify({instruction:e,model:R,thinkingLevel:B})});j(e=>({...e,name:t.agent.name,label:t.agent.label,description:t.agent.description,systemPrompt:t.agent.systemPrompt}))}catch(e){U(e instanceof Error?e.message:_(`aiFillAgentFailed`))}finally{L(!1)}}async function ee(){if(C(A)){N(!0),U(``);try{let e=S(A);O?await w(`/api/agent-profiles/${encodeURIComponent(O)}`,{method:`PATCH`,body:JSON.stringify(e)}):await w(`/api/agent-profiles`,{method:`POST`,body:JSON.stringify(e)}),Q(),await K()}catch(e){U(e instanceof Error?e.message:_(`requestFailed`))}finally{N(!1)}}}async function te(e){if(e.builtin||e.readonly)return;let t=!e.enabledAsSubagent,r=e.enabledAsSubagent;n(n=>n.map(n=>n.id===e.id?{...n,enabledAsSubagent:t}:n)),G(null);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`PATCH`,body:JSON.stringify({enabledAsSubagent:t})})}catch(t){n(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:r}:t)),U(t instanceof Error?t.message:_(`requestFailed`))}}async function ne(e){if(!(e.builtin||e.readonly)&&await f({description:_(`confirmDeleteAgent`),confirmLabel:_(`confirmDelete`),cancelLabel:_(`cancel`),variant:`destructive`})){U(``);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await K()}catch(e){U(e instanceof Error?e.message:_(`requestFailed`))}}}return(0,y.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col overflow-hidden bg-background`,children:[(0,y.jsx)(`div`,{className:`border-b border-border px-6 py-5`,children:(0,y.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,y.jsx)(`div`,{className:`flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary`,children:(0,y.jsx)(t,{className:`size-5`})}),(0,y.jsx)(`div`,{children:(0,y.jsxs)(`h1`,{className:`inline-flex items-center gap-1.5 text-lg font-semibold text-foreground`,children:[_(`agentsTab`),(0,y.jsx)(p,{label:_(`agentsDescription`)})]})})]}),(0,y.jsx)(c,{onClick:X,children:_(`createAgent`)})]})}),(0,y.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto p-6`,children:(0,y.jsxs)(`div`,{className:`mx-auto max-w-5xl space-y-5`,children:[H&&!E?(0,y.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:H}):null,(0,y.jsx)(`div`,{className:`grid gap-4 md:grid-cols-2`,children:e.map(e=>(0,y.jsxs)(`div`,{className:`rounded-xl border border-border bg-card p-4`,children:[(0,y.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,y.jsxs)(`div`,{className:`min-w-0`,children:[(0,y.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,y.jsx)(`h3`,{className:g(`truncate text-sm font-medium`,e.enabledAsSubagent?`text-foreground/90`:`text-muted-foreground`),children:e.label}),e.builtin?(0,y.jsx)(`span`,{className:`rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary`,children:_(`builtinAgent`)}):null]}),(0,y.jsx)(`p`,{className:`mt-1 font-mono text-xs text-muted-foreground`,children:e.name}),e.source&&!e.builtin?(0,y.jsxs)(`p`,{className:`mt-1 text-xs text-muted-foreground`,children:[e.source,e.relativePath?` · ${e.relativePath}`:``]}):null,(0,y.jsx)(`p`,{className:`mt-2 text-sm text-muted-foreground`,children:e.description||_(`noDescription`)})]}),(0,y.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2`,onClick:e=>e.stopPropagation(),children:[(0,y.jsx)(`button`,{type:`button`,role:`switch`,"aria-checked":e.enabledAsSubagent,disabled:e.builtin||e.readonly,className:g(`relative h-6 w-11 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60`,e.enabledAsSubagent?`bg-emerald-500`:`bg-muted-foreground/30`),onClick:()=>void te(e),title:e.enabledAsSubagent?_(`disableAsSubagent`):_(`enableAsSubagent`),children:(0,y.jsx)(`span`,{className:g(`absolute left-0.5 top-0.5 size-5 rounded-full bg-white shadow transition-transform`,e.enabledAsSubagent?`translate-x-5`:`translate-x-0`)})}),(0,y.jsxs)(`div`,{className:`relative`,children:[(0,y.jsx)(c,{variant:`ghost`,size:`icon`,onClick:()=>G(W===e.id?null:e.id),title:_(`moreActions`),children:(0,y.jsx)(a,{className:`size-4`})}),W===e.id?(0,y.jsxs)(`div`,{className:`absolute right-0 z-20 mt-1 w-36 overflow-hidden rounded-xl border border-border bg-popover py-1 text-sm shadow-quickforge`,children:[(0,y.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`,disabled:e.builtin||e.readonly,onClick:()=>{G(null),Z(e)},children:[(0,y.jsx)(r,{className:`size-3.5`}),_(`editTask`)]}),(0,y.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`,disabled:e.builtin||e.readonly,onClick:()=>{G(null),ne(e)},children:[(0,y.jsx)(i,{className:`size-3.5`}),_(`delete`)]})]}):null]})]})]}),(0,y.jsx)(`div`,{className:`mt-3 flex flex-wrap gap-1`,children:e.allowedTools.map(e=>(0,y.jsx)(`span`,{className:`rounded-full bg-muted px-2 py-0.5 font-mono text-xs text-muted-foreground`,children:e},e))}),(0,y.jsxs)(`div`,{className:`mt-3 grid gap-2 border-t border-border pt-3 text-xs text-muted-foreground sm:grid-cols-2`,children:[(0,y.jsxs)(`span`,{children:[_(`maxRuntimeMs`),e.maxRuntimeMs??`-`]}),(0,y.jsxs)(`span`,{children:[_(`maxToolCalls`),e.maxToolCalls??`-`]})]})]},e.id))})]})}),E?(0,y.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onMouseDown:e=>{e.target===e.currentTarget&&Q()},children:(0,y.jsxs)(`div`,{className:`flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-quickforge`,onMouseDown:e=>e.stopPropagation(),children:[(0,y.jsxs)(`div`,{className:`shrink-0 border-b border-border px-5 py-4`,children:[(0,y.jsx)(`h2`,{className:`text-base font-medium text-foreground`,children:_(q?`editAgent`:`createAgent`)}),q?.readonly?(0,y.jsx)(`p`,{className:`mt-1 text-sm text-muted-foreground`,children:_(`builtinAgentReadonly`)}):null]}),(0,y.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-5 py-4`,children:(0,y.jsxs)(`div`,{className:`space-y-4`,children:[(0,y.jsxs)(`div`,{className:`rounded-2xl border border-border bg-muted/20 p-3`,children:[(0,y.jsxs)(`div`,{className:`mb-2 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,y.jsx)(o,{className:`size-4 text-primary`}),_(`aiFillAgent`),(0,y.jsx)(p,{label:_(`aiFillAgentDescription`)})]}),(0,y.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?.readonly||I,onChange:e=>F(e.target.value),placeholder:_(`aiFillAgentPlaceholder`)}),(0,y.jsx)(`div`,{className:`mt-2 flex justify-end`,children:(0,y.jsxs)(c,{variant:`outline`,size:`sm`,onClick:()=>void $(),disabled:!!q?.readonly||I||!P.trim(),children:[(0,y.jsx)(o,{className:`mr-1 size-3.5`}),_(I?`aiFillAgentLoading`:`aiFillAgent`)]})})]}),(0,y.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,y.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[_(`agentName`),(0,y.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?.readonly,onChange:e=>J(`name`,e.target.value),placeholder:`reviewer`})]}),(0,y.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[_(`agentLabel`),(0,y.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?.readonly,onChange:e=>J(`label`,e.target.value),placeholder:_(`agentLabelPlaceholder`)})]})]}),(0,y.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[_(`agentDescription`),(0,y.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?.readonly,onChange:e=>J(`description`,e.target.value)})]}),(0,y.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[_(`agentSystemPrompt`),(0,y.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?.readonly,onChange:e=>J(`systemPrompt`,e.target.value)})]}),(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`div`,{className:`mb-2 text-sm font-medium text-foreground`,children:_(`allowedTools`)}),(0,y.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2`,children:s.map(e=>(0,y.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,y.jsx)(`input`,{type:`checkbox`,className:`mt-1`,disabled:!!q?.readonly,checked:A.allowedTools.includes(e.name),onChange:()=>Y(e.name)}),(0,y.jsxs)(`span`,{children:[(0,y.jsx)(`span`,{className:`font-medium text-foreground`,children:e.label}),(0,y.jsx)(`span`,{className:`ml-2 font-mono text-xs text-muted-foreground`,children:e.name}),e.riskLevel===`dangerous`?(0,y.jsx)(`span`,{className:`ml-2 rounded-full bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700`,children:_(`highRiskTool`)}):null,(0,y.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:e.description})]})]},e.name))})]}),(0,y.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,y.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[_(`maxRuntimeMs`),(0,y.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?.readonly,onChange:e=>J(`maxRuntimeMs`,e.target.value)})]}),(0,y.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[_(`maxToolCalls`),(0,y.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?.readonly,onChange:e=>J(`maxToolCalls`,e.target.value)})]})]}),(0,y.jsxs)(`label`,{className:`flex items-center gap-2 text-sm text-foreground`,children:[(0,y.jsx)(`input`,{type:`checkbox`,checked:A.enabledAsSubagent,disabled:!!q?.readonly,onChange:e=>J(`enabledAsSubagent`,e.target.checked)}),_(`enabledAsSubagent`)]}),H?(0,y.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:H}):null]})}),(0,y.jsx)(`div`,{className:`shrink-0 border-t border-border px-5 py-4`,children:(0,y.jsxs)(`div`,{className:`flex justify-end gap-2`,children:[(0,y.jsx)(c,{variant:`outline`,onClick:Q,disabled:M||I,children:_(`cancel`)}),(0,y.jsx)(c,{onClick:ee,disabled:M||I||!!q?.readonly||!C(A),children:_(`save`)})]})})]})}):null]})}export{T as AgentProfilesPage};
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{L as t,Mt as n,b as r,i,r as a,u as o,y as s}from"./icons-BlAm_pUr.js";import{n as c}from"./react-vendor--m65aOgw.js";import{$ as l,Q as u,et as d,it as f}from"./index-C_DXqhW8.js";import{n as p,r as m,t as h}from"./plugin-api-UKg_cgSG.js";var g=e(n(),1),_=c();function v(e){return e===`loaded`?`bg-emerald-500/10 text-emerald-700 dark:text-emerald-300`:e===`error`?`bg-destructive/10 text-destructive`:`bg-muted text-muted-foreground`}function y(e){return e===`loaded`?f(`pluginStatusLoaded`):e===`disabled`?f(`pluginStatusDisabled`):e===`error`?f(`pluginStatusError`):e}function b(e){switch(e){case`openai-documents`:return{label:f(`pluginOpenaiDocumentsName`),description:f(`pluginOpenaiDocumentsDescription`)};case`openai-spreadsheets`:return{label:f(`pluginOpenaiSpreadsheetsName`),description:f(`pluginOpenaiSpreadsheetsDescription`)};case`openai-presentations`:return{label:f(`pluginOpenaiPresentationsName`),description:f(`pluginOpenaiPresentationsDescription`)};default:return null}}function x(e){return b(e.name)?.label||(e.displayName||e.name).replace(/^OpenAI\s+/i,``)}function S(e){return b(e.name)?.description||e.description||f(`noDescription`)}function C(e){return(e.label||e.name||e.quickForgeName).replace(/^OpenAI\s+/i,``)}function w({onChanged:e}){let[n,c]=(0,g.useState)(null),[b,w]=(0,g.useState)(!0),[T,E]=(0,g.useState)(null),[D,O]=(0,g.useState)(null),[k,A]=(0,g.useState)(null),j=(0,g.useCallback)(async(t=`load`)=>{A(null),w(!0);try{c(t===`reload`?await p():await h()),t===`reload`&&e?.()}catch(e){A(e instanceof Error?e.message:f(`pluginsLoadFailed`))}finally{w(!1)}},[e]);(0,g.useEffect)(()=>{let e=!1;return h().then(t=>{e||c(t)}).catch(t=>{e||A(t instanceof Error?t.message:f(`pluginsLoadFailed`))}).finally(()=>{e||w(!1)}),()=>{e=!0}},[]);let M=(0,g.useMemo)(()=>{let e=n?.plugins||[];return{total:e.length,enabled:e.filter(e=>e.enabled).length,tools:e.reduce((e,t)=>e+t.tools.length,0)}},[n]),N=(0,g.useMemo)(()=>D&&(n?.plugins||[]).find(e=>e.name===D)||null,[n,D]),P=async(t,n)=>{E(t),A(null);try{c(await m(t,n)),e?.()}catch(e){A(e instanceof Error?e.message:f(`pluginsSaveFailed`))}finally{E(null)}};return(0,_.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col overflow-hidden bg-background`,children:[(0,_.jsxs)(`div`,{className:`border-b border-border px-6 py-5`,children:[(0,_.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,_.jsx)(`div`,{className:`flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary`,children:(0,_.jsx)(r,{className:`size-5`})}),(0,_.jsx)(`div`,{children:(0,_.jsxs)(`h1`,{className:`inline-flex items-center gap-1.5 text-lg font-semibold text-foreground`,children:[f(`plugins`),(0,_.jsx)(u,{label:f(`pluginsDescription`)})]})})]}),(0,_.jsxs)(l,{variant:`outline`,size:`sm`,onClick:()=>void j(`reload`),disabled:b,children:[b?(0,_.jsx)(t,{className:`mr-2 size-4 animate-spin`}):(0,_.jsx)(s,{className:`mr-2 size-4`}),f(`pluginsReload`)]})]}),(0,_.jsxs)(`div`,{className:`mt-4 flex flex-wrap gap-2 text-xs text-muted-foreground`,children:[(0,_.jsx)(`span`,{className:`rounded-full bg-muted px-2 py-0.5`,children:f(`pluginsCount`,M)}),(0,_.jsx)(`span`,{className:`rounded-full bg-muted px-2 py-0.5`,children:f(`pluginToolsCount`,{count:M.tools})})]})]}),(0,_.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto p-6`,children:(0,_.jsxs)(`div`,{className:`mx-auto max-w-5xl space-y-5`,children:[k?(0,_.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:k}):null,n?.errors?.length?(0,_.jsxs)(`div`,{className:`space-y-2 rounded-lg border border-amber-500/30 bg-amber-500/8 p-3 text-sm`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 font-medium text-amber-700 dark:text-amber-300`,children:[(0,_.jsx)(i,{className:`size-4`}),f(`pluginDiscoveryErrors`)]}),n.errors.map((e,t)=>(0,_.jsxs)(`div`,{className:`text-muted-foreground`,children:[(0,_.jsx)(`span`,{className:`font-mono text-xs`,children:e.dir}),`: `,e.error]},`${e.dir}-${t}`))]}):null,b&&!n?(0,_.jsxs)(`div`,{className:`flex h-40 items-center justify-center text-sm text-muted-foreground`,children:[(0,_.jsx)(t,{className:`mr-2 size-4 animate-spin`}),f(`loadingPlugins`)]}):null,!b&&n&&n.plugins.length===0?(0,_.jsxs)(`div`,{className:`rounded-lg border border-border bg-muted/15 p-4 text-xs text-muted-foreground/55`,children:[(0,_.jsx)(`div`,{className:`text-base font-medium text-foreground`,children:f(`noPlugins`)}),(0,_.jsx)(`p`,{className:`mt-2`,children:f(`noPluginsDescription`)}),(0,_.jsx)(`div`,{className:`mt-4 space-y-1`,children:(n.searchPaths||[]).map(e=>(0,_.jsx)(`div`,{className:`font-mono text-xs`,children:e},e))})]}):null,(0,_.jsx)(`div`,{className:`grid gap-4 md:grid-cols-2`,children:(n?.plugins||[]).map(e=>(0,_.jsxs)(`article`,{className:`rounded-xl border border-border bg-card p-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,_.jsxs)(`div`,{className:`min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,_.jsx)(`h2`,{className:`truncate text-sm font-medium text-foreground/90`,children:x(e)}),(0,_.jsxs)(`span`,{className:`rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground`,children:[`v`,e.version]}),(0,_.jsx)(`span`,{className:d(`rounded-full px-2 py-0.5 text-xs`,v(e.status)),children:y(e.status)})]}),(0,_.jsx)(`p`,{className:`mt-2 line-clamp-2 text-sm text-muted-foreground`,children:S(e)})]}),(0,_.jsxs)(`div`,{className:`flex shrink-0 flex-col gap-1.5`,children:[(0,_.jsxs)(l,{variant:e.enabled?`outline`:`default`,size:`sm`,disabled:T===e.name,onClick:()=>void P(e.name,!e.enabled),children:[T===e.name?(0,_.jsx)(t,{className:`mr-2 size-4 animate-spin`}):null,e.enabled?f(`disablePlugin`):f(`enablePlugin`)]}),(0,_.jsx)(l,{variant:`ghost`,size:`sm`,onClick:()=>O(e.name),children:f(`viewDetails`)})]})]}),e.error?(0,_.jsx)(`div`,{className:`mt-3 rounded-lg border border-destructive/30 bg-destructive/8 px-3 py-2 text-sm text-destructive`,children:e.error}):null,(0,_.jsxs)(`div`,{className:`mt-3 flex flex-wrap gap-x-4 gap-y-1 border-t border-border pt-3 text-xs text-muted-foreground`,children:[(0,_.jsxs)(`span`,{className:`inline-flex items-center gap-1`,children:[(0,_.jsx)(a,{className:`size-3`}),f(`pluginToolsCount`,{count:e.tools.length})]}),(0,_.jsxs)(`span`,{className:`inline-flex items-center gap-1`,children:[(0,_.jsx)(o,{className:`size-3`}),f(`pluginPermissionsCount`,{count:e.permissions.length})]}),(0,_.jsx)(`span`,{children:e.enabled?f(`enabled`):f(`disabled`)})]})]},e.name))})]})}),N?(0,_.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onMouseDown:e=>{e.target===e.currentTarget&&O(null)},children:(0,_.jsxs)(`div`,{className:`flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-border bg-background shadow-quickforge`,onMouseDown:e=>e.stopPropagation(),children:[(0,_.jsxs)(`div`,{className:`flex shrink-0 items-start justify-between gap-3 border-b border-border px-5 py-4`,children:[(0,_.jsxs)(`div`,{className:`min-w-0`,children:[(0,_.jsx)(`h2`,{className:`text-base font-medium text-foreground`,children:f(`pluginDetails`)}),(0,_.jsx)(`p`,{className:`mt-1 truncate text-sm text-muted-foreground`,children:x(N)})]}),(0,_.jsx)(l,{variant:`ghost`,size:`sm`,onClick:()=>O(null),children:f(`close`)})]}),(0,_.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto px-5 py-4`,children:(0,_.jsxs)(`div`,{className:`space-y-4`,children:[N.error?(0,_.jsx)(`div`,{className:`rounded-lg border border-destructive/30 bg-destructive/8 px-3 py-2 text-sm text-destructive`,children:N.error}):null,(0,_.jsxs)(`div`,{className:`rounded-xl border border-border bg-card p-4`,children:[(0,_.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:S(N)}),(0,_.jsxs)(`div`,{className:`mt-3 grid gap-2 border-t border-border pt-3 text-xs text-muted-foreground sm:grid-cols-2`,children:[(0,_.jsxs)(`span`,{children:[f(`pluginVersion`),`:`,N.version]}),(0,_.jsxs)(`span`,{children:[f(`status`),`:`,y(N.status)]}),(0,_.jsx)(`span`,{children:f(`pluginToolsCount`,{count:N.tools.length})}),(0,_.jsx)(`span`,{children:N.enabled?f(`enabled`):f(`disabled`)})]})]}),(0,_.jsxs)(`div`,{className:`rounded-xl border border-border bg-card p-4`,children:[(0,_.jsxs)(`div`,{className:`mb-3 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,_.jsx)(a,{className:`size-4 text-muted-foreground`}),f(`pluginTools`)]}),N.tools.length?(0,_.jsx)(`div`,{className:`space-y-2`,children:N.tools.map(e=>(0,_.jsxs)(`div`,{className:`rounded-xl bg-muted/30 px-3 py-2`,children:[(0,_.jsx)(`div`,{className:`text-sm font-medium text-foreground`,children:C(e)}),e.description?(0,_.jsx)(`div`,{className:`mt-1 text-xs text-muted-foreground`,children:e.description}):null]},e.quickForgeName))}):(0,_.jsx)(`div`,{className:`text-sm text-muted-foreground`,children:f(`pluginNoTools`)})]}),(0,_.jsxs)(`div`,{className:`rounded-xl border border-border bg-card p-4`,children:[(0,_.jsxs)(`div`,{className:`mb-3 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,_.jsx)(o,{className:`size-4 text-muted-foreground`}),f(`pluginPermissions`)]}),N.permissions.length?(0,_.jsx)(`div`,{className:`flex flex-wrap gap-1.5`,children:N.permissions.map(e=>(0,_.jsx)(`span`,{className:`rounded-full bg-muted px-2 py-0.5 font-mono text-xs text-muted-foreground`,children:e},e))}):(0,_.jsx)(`div`,{className:`text-sm text-muted-foreground`,children:f(`pluginNoPermissions`)}),(0,_.jsx)(`p`,{className:`mt-3 text-xs text-muted-foreground/80`,children:f(`pluginTrustedNotice`)})]})]})})]})}):null]})}export{w as PluginsPage};