@shawnstack/quickforge 1.7.1 → 1.7.3
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.
- package/README.md +1 -0
- package/bin/quickforge.mjs +2 -0
- package/dist/assets/AgentProfilesPage-D7pbg8nm.js +1 -0
- package/dist/assets/ChatPanelHost-wfGCaCzD.js +288 -0
- package/dist/assets/{PluginsPage-CKyWhlCo.js → PluginsPage-NAGroBm3.js} +1 -1
- package/dist/assets/ScheduledTasksPage-Bp0MNfQD.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-DwqEnUmX.js → SettingsWorkspacePage-BtJZMIsZ.js} +425 -344
- package/dist/assets/SharedConversationPage-Ce3KoVZt.js +1 -0
- package/dist/assets/{TerminalDock-B9xKnimU.js → TerminalDock-4eUuXP7X.js} +2 -2
- package/dist/assets/WorkspaceInspector-kM3BRPjY.js +13 -0
- package/dist/assets/icons-BP8YOS-Z.js +1 -0
- package/dist/assets/index-C6k5taeb.js +66 -0
- package/dist/assets/index-CzN8NSKC.css +3 -0
- package/dist/assets/mcp-servers-dialog-BQbFywVL.js +5 -0
- package/dist/assets/{monaco-CPwJMUsl.js → monaco-BTsVCDWS.js} +1 -1
- package/dist/assets/{react-vendor-CLbWF1Oy.js → react-vendor-Dr5xvL-e.js} +1 -1
- package/dist/assets/{skills-dialog-D18mxNyW.js → skills-dialog-REBeTFSH.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +4 -1
- package/server/acp/server.mjs +19 -7
- package/server/ai-http-logger.mjs +6 -6
- package/server/ai-provider-options.mjs +8 -0
- package/server/index.mjs +7 -2
- package/server/mcp/registry.mjs +54 -14
- package/server/network-proxy.mjs +384 -0
- package/server/plugins/loader.mjs +9 -1
- package/server/plugins/registry.mjs +32 -10
- package/server/public-api.mjs +4 -0
- package/server/routes/agent-profiles.mjs +2 -0
- package/server/routes/mcp.mjs +0 -5
- package/server/routes/models.mjs +1 -0
- package/server/routes/scheduled-tasks.mjs +74 -32
- package/server/routes/system.mjs +26 -0
- package/server/routes/workspace.mjs +89 -17
- package/server/session-utils.mjs +2 -0
- package/server/utils/scheduled-tasks.mjs +23 -10
- package/server/utils/workspace.mjs +25 -3
- package/dist/assets/AgentProfilesPage-__uY0AvK.js +0 -1
- package/dist/assets/ChatPanelHost-DIs_vWFX.js +0 -291
- package/dist/assets/ScheduledTasksPage-KtJoeJYt.js +0 -2
- package/dist/assets/SharedConversationPage-yujCRsbe.js +0 -1
- package/dist/assets/WorkspaceInspector-CE6RD6Ys.js +0 -13
- package/dist/assets/icons-pPRMD2tE.js +0 -1
- package/dist/assets/index-BSXpUDCq.js +0 -63
- package/dist/assets/index-DpO7jEGP.css +0 -3
- package/dist/assets/mcp-servers-dialog-CaQTvGiW.js +0 -5
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { streamSimple } from '@earendil-works/pi-ai/compat'
|
|
2
|
+
import { DEFAULT_AI_MAX_RETRIES } from '../ai-provider-options.mjs'
|
|
2
3
|
import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
|
|
3
4
|
import { readStore, atomicUpdate } from '../storage.mjs'
|
|
4
|
-
import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'
|
|
5
|
+
import { createAgent, getSessionEventBus, agentEvents, persistSessionState, abortRun } from '../agent-manager.mjs'
|
|
5
6
|
import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
|
|
6
7
|
import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
|
|
7
8
|
import { logger } from '../utils/logger.mjs'
|
|
@@ -165,6 +166,7 @@ async function parseScheduledTaskInstructionWithAi(instruction, model, thinkingL
|
|
|
165
166
|
maxTokens: 600,
|
|
166
167
|
temperature: 0,
|
|
167
168
|
reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
|
|
169
|
+
maxRetries: DEFAULT_AI_MAX_RETRIES,
|
|
168
170
|
maxRetryDelayMs: 60000,
|
|
169
171
|
},
|
|
170
172
|
)
|
|
@@ -436,21 +438,30 @@ async function updateTask(taskId, updater) {
|
|
|
436
438
|
return updated
|
|
437
439
|
}
|
|
438
440
|
|
|
441
|
+
function recurringTaskStatusRepair(task, now) {
|
|
442
|
+
if (task?.status !== 'completed' || !isRecurringTask(task)) return null
|
|
443
|
+
const nextRunAt = task.nextRunAt && new Date(task.nextRunAt).getTime() > now.getTime()
|
|
444
|
+
? task.nextRunAt
|
|
445
|
+
: calculateNextRun(task, now)
|
|
446
|
+
if (!nextRunAt) return null
|
|
447
|
+
return {
|
|
448
|
+
...task,
|
|
449
|
+
status: 'enabled',
|
|
450
|
+
nextRunAt,
|
|
451
|
+
scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
|
|
452
|
+
updatedAt: now.toISOString(),
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
439
456
|
async function repairRecurringTaskStatuses() {
|
|
457
|
+
const now = new Date()
|
|
458
|
+
const snapshot = await readStore(STORE)
|
|
459
|
+
if (!Object.values(snapshot).some((task) => recurringTaskStatusRepair(task, now))) return
|
|
460
|
+
|
|
440
461
|
await atomicUpdate(STORE, (data) => {
|
|
441
462
|
for (const [taskId, task] of Object.entries(data)) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
? task.nextRunAt
|
|
445
|
-
: calculateNextRun(task)
|
|
446
|
-
if (!nextRunAt) continue
|
|
447
|
-
data[taskId] = {
|
|
448
|
-
...task,
|
|
449
|
-
status: 'enabled',
|
|
450
|
-
nextRunAt,
|
|
451
|
-
scheduleRule: scheduleRuleFor({ ...task, nextRunAt }),
|
|
452
|
-
updatedAt: new Date().toISOString(),
|
|
453
|
-
}
|
|
463
|
+
const repaired = recurringTaskStatusRepair(task, now)
|
|
464
|
+
if (repaired) data[taskId] = repaired
|
|
454
465
|
}
|
|
455
466
|
return data
|
|
456
467
|
})
|
|
@@ -566,14 +577,15 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
566
577
|
onStarted?.({ taskId: task.id, runId, sessionId })
|
|
567
578
|
|
|
568
579
|
const eventBus = getSessionEventBus(sessionId)
|
|
580
|
+
const runtimeLimitMs = Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000))
|
|
581
|
+
let timeout = null
|
|
582
|
+
let handler = null
|
|
583
|
+
let timedOut = false
|
|
584
|
+
let resolveFinished
|
|
569
585
|
const finished = new Promise((resolve) => {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
eventBus?.removeListener('agent_event', handler)
|
|
573
|
-
}
|
|
574
|
-
const handler = (event) => {
|
|
586
|
+
resolveFinished = resolve
|
|
587
|
+
handler = (event) => {
|
|
575
588
|
if (event.type !== 'agent_end') return
|
|
576
|
-
cleanup(handler, timeout)
|
|
577
589
|
const errorMessage = event.errorMessage || session.agent.state.errorMessage
|
|
578
590
|
const aborted = session.status === 'aborted' || session.agent.state.messages.some((message) => message?.role === 'assistant' && message?.stopReason === 'aborted')
|
|
579
591
|
resolve({
|
|
@@ -583,24 +595,54 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
583
595
|
messages: event.messages ?? session.agent.state.messages,
|
|
584
596
|
})
|
|
585
597
|
}
|
|
586
|
-
const timeout = setTimeout(() => {
|
|
587
|
-
cleanup(handler, timeout)
|
|
588
|
-
resolve({ ok: false, aborted: false, error: '执行超时', messages: session.agent.state.messages })
|
|
589
|
-
}, Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000)))
|
|
590
598
|
eventBus?.on('agent_event', handler)
|
|
591
599
|
})
|
|
592
600
|
|
|
601
|
+
const runPromise = (async () => {
|
|
602
|
+
try {
|
|
603
|
+
await session.agent.continue()
|
|
604
|
+
} catch (continueError) {
|
|
605
|
+
if (continueError?.message !== 'Request was aborted' && continueError?.message !== 'Scheduled task aborted') {
|
|
606
|
+
throw continueError
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return finished
|
|
610
|
+
})()
|
|
611
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
612
|
+
timeout = setTimeout(() => resolve({ timedOut: true }), runtimeLimitMs)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
let result
|
|
593
616
|
try {
|
|
594
|
-
await
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
617
|
+
const outcome = await Promise.race([
|
|
618
|
+
runPromise.then((value) => ({ result: value })),
|
|
619
|
+
timeoutPromise,
|
|
620
|
+
])
|
|
621
|
+
if (outcome.timedOut) {
|
|
622
|
+
timedOut = true
|
|
623
|
+
const abortPromise = Promise.resolve()
|
|
624
|
+
.then(() => abortRun(sessionId))
|
|
625
|
+
.catch((error) => {
|
|
626
|
+
logger.warn(`Failed to abort timed out scheduled task ${task.id}:`, error)
|
|
627
|
+
})
|
|
628
|
+
await Promise.race([
|
|
629
|
+
abortPromise,
|
|
630
|
+
new Promise((resolve) => setTimeout(resolve, 1000)),
|
|
631
|
+
])
|
|
632
|
+
await Promise.race([
|
|
633
|
+
runPromise.catch(() => undefined),
|
|
634
|
+
new Promise((resolve) => setTimeout(resolve, 1000)),
|
|
635
|
+
])
|
|
636
|
+
result = { ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages }
|
|
598
637
|
} else {
|
|
599
|
-
|
|
638
|
+
result = outcome.result
|
|
600
639
|
}
|
|
640
|
+
} finally {
|
|
641
|
+
clearTimeout(timeout)
|
|
642
|
+
eventBus?.removeListener('agent_event', handler)
|
|
643
|
+
if (timedOut) resolveFinished?.({ ok: false, aborted: true, error: '执行超时', messages: session.agent.state.messages })
|
|
601
644
|
}
|
|
602
|
-
|
|
603
|
-
if (result.aborted) settled = true
|
|
645
|
+
settled = true
|
|
604
646
|
const finishedAt = new Date().toISOString()
|
|
605
647
|
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
|
|
606
648
|
const aiResult = result.ok ? latestAssistantText(result.messages) : ''
|
|
@@ -634,7 +676,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
634
676
|
inputContent: run.inputContent ?? latestTask.instruction,
|
|
635
677
|
aiResult: result.ok ? aiResult : undefined,
|
|
636
678
|
result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
|
|
637
|
-
errorMessage: result.
|
|
679
|
+
errorMessage: result.error,
|
|
638
680
|
sessionId,
|
|
639
681
|
agentId: executionAgent?.id || latestTask.agentId || null,
|
|
640
682
|
agentLabel: executionAgent?.label || null,
|
|
@@ -651,7 +693,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
|
|
|
651
693
|
sessionId,
|
|
652
694
|
status: result.aborted ? 'failed' : (result.ok ? 'success' : 'failed'),
|
|
653
695
|
result: aiResult,
|
|
654
|
-
errorMessage: result.
|
|
696
|
+
errorMessage: result.error,
|
|
655
697
|
})
|
|
656
698
|
} catch (error) {
|
|
657
699
|
const finishedAt = new Date().toISOString()
|
package/server/routes/system.mjs
CHANGED
|
@@ -70,6 +70,32 @@ export async function handleSystemApi(req, res, url, context) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
if (req.method === 'GET' && url.pathname === '/api/system/network-proxy') {
|
|
74
|
+
sendJson(res, 200, await context.getNetworkProxyConfig())
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (req.method === 'PUT' && url.pathname === '/api/system/network-proxy') {
|
|
79
|
+
if (!context.isLocalRequest) {
|
|
80
|
+
const error = new Error('Network proxy settings can only be changed from this computer')
|
|
81
|
+
error.statusCode = 403
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
const body = await readJsonBody(req, 64 * 1024) || {}
|
|
85
|
+
sendJson(res, 200, await context.updateNetworkProxyConfig(body))
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (req.method === 'POST' && url.pathname === '/api/system/network-proxy/refresh') {
|
|
90
|
+
if (!context.isLocalRequest) {
|
|
91
|
+
const error = new Error('System proxy can only be refreshed from this computer')
|
|
92
|
+
error.statusCode = 403
|
|
93
|
+
throw error
|
|
94
|
+
}
|
|
95
|
+
sendJson(res, 200, await context.refreshSystemProxy())
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
if (req.method === 'GET' && url.pathname === '/api/system/network') {
|
|
74
100
|
sendJson(res, 200, {
|
|
75
101
|
host: context.host,
|
|
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import { streamSimple } from '@earendil-works/pi-ai/compat'
|
|
5
|
+
import { DEFAULT_AI_MAX_RETRIES } from '../ai-provider-options.mjs'
|
|
5
6
|
import { sendJson, readJsonBody } from '../utils/response.mjs'
|
|
6
7
|
import { projectContextFromId } from '../project-config.mjs'
|
|
7
8
|
import { readStore } from '../storage.mjs'
|
|
@@ -9,12 +10,17 @@ import { logger } from '../utils/logger.mjs'
|
|
|
9
10
|
import { openPathInFileManager, openPathInIDEA, openPathInVSCode } from '../utils/platform.mjs'
|
|
10
11
|
import {
|
|
11
12
|
assertSafeWorkspacePath,
|
|
13
|
+
createWorkspacePathValidator,
|
|
12
14
|
resolveWorkspacePath,
|
|
13
15
|
toWorkspaceRelative,
|
|
14
16
|
} from '../utils/workspace.mjs'
|
|
15
17
|
|
|
16
18
|
const MAX_PREVIEW_BYTES = 50 * 1024 * 1024
|
|
17
19
|
const MAX_STATIC_PREVIEW_BYTES = 50 * 1024 * 1024
|
|
20
|
+
const MAX_GIT_LINE_COUNT_FILES = 100
|
|
21
|
+
const MAX_GIT_LINE_COUNT_FILE_BYTES = 1024 * 1024
|
|
22
|
+
const MAX_GIT_LINE_COUNT_TOTAL_BYTES = 10 * 1024 * 1024
|
|
23
|
+
const GIT_LINE_COUNT_CONCURRENCY = 6
|
|
18
24
|
const PREVIEW_ALLOWED_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.ico', '.txt', '.md'])
|
|
19
25
|
const MAX_TREE_NODES = 50000
|
|
20
26
|
const SKIP_DIRS = new Set(['.git', 'node_modules'])
|
|
@@ -294,16 +300,78 @@ async function collectNumstat(context) {
|
|
|
294
300
|
return map
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
async function readUtf8FileAtMost(fullPath, maxBytes) {
|
|
304
|
+
if (maxBytes === 0) return ''
|
|
305
|
+
const handle = await fs.open(fullPath, 'r')
|
|
299
306
|
try {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
const buffer = Buffer.allocUnsafe(maxBytes)
|
|
308
|
+
let offset = 0
|
|
309
|
+
while (offset < maxBytes) {
|
|
310
|
+
const { bytesRead } = await handle.read(buffer, offset, maxBytes - offset, offset)
|
|
311
|
+
if (bytesRead === 0) break
|
|
312
|
+
offset += bytesRead
|
|
313
|
+
}
|
|
314
|
+
return buffer.subarray(0, offset).toString('utf8')
|
|
315
|
+
} finally {
|
|
316
|
+
await handle.close()
|
|
304
317
|
}
|
|
305
318
|
}
|
|
306
319
|
|
|
320
|
+
async function poolMap(items, fn, concurrency) {
|
|
321
|
+
const results = new Array(items.length)
|
|
322
|
+
let cursor = 0
|
|
323
|
+
async function worker() {
|
|
324
|
+
while (cursor < items.length) {
|
|
325
|
+
const index = cursor
|
|
326
|
+
cursor += 1
|
|
327
|
+
results[index] = await fn(items[index], index)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()))
|
|
331
|
+
return results
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 未跟踪文件不在 numstat 中,在有界预算内按工作区文件行数估算新增行
|
|
335
|
+
async function collectWorkspaceLineCounts(context, files) {
|
|
336
|
+
const candidates = files
|
|
337
|
+
.filter((file) => file.status === 'untracked' || file.status === 'added')
|
|
338
|
+
.slice(0, MAX_GIT_LINE_COUNT_FILES)
|
|
339
|
+
if (candidates.length === 0) return new Map()
|
|
340
|
+
|
|
341
|
+
const validateWorkspacePath = await createWorkspacePathValidator(context)
|
|
342
|
+
const inspected = await poolMap(candidates, async (file) => {
|
|
343
|
+
try {
|
|
344
|
+
const fullPath = resolveWorkspacePath(file.path, context)
|
|
345
|
+
await validateWorkspacePath(fullPath, { allowSensitive: true })
|
|
346
|
+
const stat = await fs.stat(fullPath)
|
|
347
|
+
if (!stat.isFile() || stat.size > MAX_GIT_LINE_COUNT_FILE_BYTES) return null
|
|
348
|
+
return { file, fullPath, size: stat.size }
|
|
349
|
+
} catch {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
}, GIT_LINE_COUNT_CONCURRENCY)
|
|
353
|
+
|
|
354
|
+
let totalBytes = 0
|
|
355
|
+
const selected = []
|
|
356
|
+
for (const entry of inspected) {
|
|
357
|
+
if (!entry || totalBytes + entry.size > MAX_GIT_LINE_COUNT_TOTAL_BYTES) continue
|
|
358
|
+
totalBytes += entry.size
|
|
359
|
+
selected.push(entry)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const counts = await poolMap(selected, async ({ file, fullPath, size }) => {
|
|
363
|
+
try {
|
|
364
|
+
const stat = await fs.stat(fullPath)
|
|
365
|
+
if (!stat.isFile() || stat.size > size) return null
|
|
366
|
+
const content = await readUtf8FileAtMost(fullPath, size)
|
|
367
|
+
return [file.path, countTextLines(content)]
|
|
368
|
+
} catch {
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
}, GIT_LINE_COUNT_CONCURRENCY)
|
|
372
|
+
return new Map(counts.filter(Boolean))
|
|
373
|
+
}
|
|
374
|
+
|
|
307
375
|
export async function listGitStatus(context) {
|
|
308
376
|
if (!(await isGitRepository(context.workspaceRoot))) return { isGitRepository: false, files: [] }
|
|
309
377
|
const result = await git(
|
|
@@ -312,17 +380,19 @@ export async function listGitStatus(context) {
|
|
|
312
380
|
)
|
|
313
381
|
const files = parseGitStatus(result.stdout)
|
|
314
382
|
const numstat = await collectNumstat(context)
|
|
383
|
+
const fallbackFiles = files.filter((file) => !numstat.has(file.path))
|
|
384
|
+
const workspaceLineCounts = await collectWorkspaceLineCounts(context, fallbackFiles)
|
|
315
385
|
for (const file of files) {
|
|
316
386
|
const entry = numstat.get(file.path)
|
|
317
387
|
if (entry) {
|
|
318
388
|
file.additions = entry.additions
|
|
319
389
|
file.deletions = entry.deletions
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
const count = workspaceLineCounts.get(file.path)
|
|
393
|
+
if (typeof count === 'number') {
|
|
394
|
+
file.additions = count
|
|
395
|
+
file.deletions = 0
|
|
326
396
|
}
|
|
327
397
|
}
|
|
328
398
|
const head = await currentGitHead(context.workspaceRoot)
|
|
@@ -561,6 +631,7 @@ ${trimForPrompt(worktreeDiff)}`
|
|
|
561
631
|
maxTokens: 500,
|
|
562
632
|
temperature: 0,
|
|
563
633
|
reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
|
|
634
|
+
maxRetries: DEFAULT_AI_MAX_RETRIES,
|
|
564
635
|
maxRetryDelayMs: 60000,
|
|
565
636
|
},
|
|
566
637
|
)
|
|
@@ -601,7 +672,7 @@ async function readWorkspaceTextFile(context, relativePath) {
|
|
|
601
672
|
return { content: buffer.toString('utf8'), size: stat.size, path: toWorkspaceRelative(file, context) }
|
|
602
673
|
}
|
|
603
674
|
|
|
604
|
-
async function buildTreeForDirectory(dir, context, counter) {
|
|
675
|
+
async function buildTreeForDirectory(dir, context, counter, validateWorkspacePath) {
|
|
605
676
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
606
677
|
const nodes = []
|
|
607
678
|
const sortedEntries = entries.sort((left, right) => {
|
|
@@ -616,20 +687,20 @@ async function buildTreeForDirectory(dir, context, counter) {
|
|
|
616
687
|
if (entry.isDirectory()) {
|
|
617
688
|
if (SKIP_DIRS.has(entry.name)) continue
|
|
618
689
|
try {
|
|
619
|
-
await
|
|
690
|
+
await validateWorkspacePath(fullPath, { allowSensitive: true })
|
|
620
691
|
counter.count += 1
|
|
621
692
|
nodes.push({
|
|
622
693
|
name: entry.name,
|
|
623
694
|
path: relativePath,
|
|
624
695
|
type: 'directory',
|
|
625
|
-
children: await buildTreeForDirectory(fullPath, context, counter),
|
|
696
|
+
children: await buildTreeForDirectory(fullPath, context, counter, validateWorkspacePath),
|
|
626
697
|
})
|
|
627
698
|
} catch {
|
|
628
699
|
// Skip directories that cannot be safely resolved.
|
|
629
700
|
}
|
|
630
701
|
} else if (entry.isFile()) {
|
|
631
702
|
try {
|
|
632
|
-
await
|
|
703
|
+
await validateWorkspacePath(fullPath, { allowSensitive: true })
|
|
633
704
|
counter.count += 1
|
|
634
705
|
nodes.push({ name: entry.name, path: relativePath, type: 'file' })
|
|
635
706
|
} catch {
|
|
@@ -642,7 +713,8 @@ async function buildTreeForDirectory(dir, context, counter) {
|
|
|
642
713
|
|
|
643
714
|
async function handleWorkspaceTree(req, res, url) {
|
|
644
715
|
const context = await projectContextFromUrl(url)
|
|
645
|
-
const
|
|
716
|
+
const validateWorkspacePath = await createWorkspacePathValidator(context)
|
|
717
|
+
const tree = await buildTreeForDirectory(context.workspaceRoot, context, { count: 0 }, validateWorkspacePath)
|
|
646
718
|
sendJson(res, 200, { root: context.project.name, tree })
|
|
647
719
|
}
|
|
648
720
|
|
package/server/session-utils.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { streamSimple } from '@earendil-works/pi-ai/compat'
|
|
2
|
+
import { DEFAULT_AI_MAX_RETRIES } from './ai-provider-options.mjs'
|
|
2
3
|
import { buildInstructionsPayload, projectContextFromId } from './project-config.mjs'
|
|
3
4
|
import { composeSystemPrompt } from './system-prompt.mjs'
|
|
4
5
|
import { listSubagentProfiles } from './agent-profiles.mjs'
|
|
@@ -99,6 +100,7 @@ export async function generateAiTitle(messages, model, thinkingLevel, getApiKey)
|
|
|
99
100
|
maxTokens: 160,
|
|
100
101
|
temperature: 0.2,
|
|
101
102
|
reasoning: thinkingLevel === 'off' ? undefined : 'medium',
|
|
103
|
+
maxRetries: DEFAULT_AI_MAX_RETRIES,
|
|
102
104
|
maxRetryDelayMs: 60000,
|
|
103
105
|
},
|
|
104
106
|
)
|
|
@@ -90,6 +90,7 @@ function parseCronField(field, min, max) {
|
|
|
90
90
|
for (const part of field.split(',')) {
|
|
91
91
|
if (/^\*\/\d+$/.test(part)) {
|
|
92
92
|
const step = Number(part.slice(2))
|
|
93
|
+
if (!Number.isInteger(step) || step <= 0) return null
|
|
93
94
|
for (let value = min; value <= max; value += step) values.add(value)
|
|
94
95
|
} else if (/^\d+-\d+$/.test(part)) {
|
|
95
96
|
const [start, end] = part.split('-').map(Number)
|
|
@@ -102,25 +103,37 @@ function parseCronField(field, min, max) {
|
|
|
102
103
|
return { any: false, values: [...values] }
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
function parseCronExpression(cronExpression) {
|
|
106
107
|
const fields = String(cronExpression || '').trim().split(/\s+/)
|
|
107
|
-
if (fields.length !== 5) return
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
if (fields.length !== 5) return null
|
|
109
|
+
const rules = [
|
|
110
|
+
parseCronField(fields[0], 0, 59),
|
|
111
|
+
parseCronField(fields[1], 0, 23),
|
|
112
|
+
parseCronField(fields[2], 1, 31),
|
|
113
|
+
parseCronField(fields[3], 1, 12),
|
|
114
|
+
parseCronField(fields[4], 0, 6),
|
|
114
115
|
]
|
|
115
|
-
return
|
|
116
|
+
return rules.every(Boolean) ? rules : null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cronRulesMatch(date, rules) {
|
|
120
|
+
const values = [date.getMinutes(), date.getHours(), date.getDate(), date.getMonth() + 1, date.getDay()]
|
|
121
|
+
return rules.every((rule, index) => rule.any || rule.values.includes(values[index]))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function cronMatches(date, cronExpression) {
|
|
125
|
+
const rules = parseCronExpression(cronExpression)
|
|
126
|
+
return rules ? cronRulesMatch(date, rules) : false
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
export function nextCronRun(cronExpression, base = new Date()) {
|
|
130
|
+
const rules = parseCronExpression(cronExpression)
|
|
131
|
+
if (!rules) return null
|
|
119
132
|
const cursor = new Date(base.getTime() + minuteMs)
|
|
120
133
|
cursor.setSeconds(0, 0)
|
|
121
134
|
const maxChecks = 366 * 24 * 60
|
|
122
135
|
for (let index = 0; index < maxChecks; index += 1) {
|
|
123
|
-
if (
|
|
136
|
+
if (cronRulesMatch(cursor, rules)) return cursor
|
|
124
137
|
cursor.setMinutes(cursor.getMinutes() + 1)
|
|
125
138
|
}
|
|
126
139
|
return null
|
|
@@ -75,15 +75,13 @@ async function realpathNearestExistingParent(inputPath) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
async function assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal, options = {}) {
|
|
79
79
|
if (!options.allowSensitive && isSensitiveWorkspacePath(fullPath, context)) {
|
|
80
80
|
const error = new Error(`Access to sensitive path is blocked: ${toWorkspaceRelative(fullPath, context)}`)
|
|
81
81
|
error.statusCode = 403
|
|
82
82
|
throw error
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const workspaceRoot = getToolWorkspaceRoot(context)
|
|
86
|
-
const workspaceReal = await fs.realpath(workspaceRoot)
|
|
87
85
|
let targetReal
|
|
88
86
|
try {
|
|
89
87
|
targetReal = await fs.realpath(fullPath)
|
|
@@ -104,6 +102,16 @@ export async function assertSafeWorkspacePath(fullPath, context, options = {}) {
|
|
|
104
102
|
}
|
|
105
103
|
}
|
|
106
104
|
|
|
105
|
+
export async function createWorkspacePathValidator(context) {
|
|
106
|
+
const workspaceReal = await fs.realpath(getToolWorkspaceRoot(context))
|
|
107
|
+
return (fullPath, options = {}) => assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal, options)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function assertSafeWorkspacePath(fullPath, context, options = {}) {
|
|
111
|
+
const validateWorkspacePath = await createWorkspacePathValidator(context)
|
|
112
|
+
return validateWorkspacePath(fullPath, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
export function truncateText(text, maxChars = 50000) {
|
|
108
116
|
if (text.length <= maxChars) return text
|
|
109
117
|
return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`
|
|
@@ -134,12 +142,25 @@ export async function assertDirectory(dir) {
|
|
|
134
142
|
const SIZE_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', 'dist-ssr', '.vite', '.cache', '.next', '.nuxt', '__pycache__', '.venv', 'venv'])
|
|
135
143
|
const directorySizeCache = new Map()
|
|
136
144
|
const DIRECTORY_SIZE_CACHE_TTL_MS = 10_000
|
|
145
|
+
const DIRECTORY_SIZE_CACHE_MAX_ENTRIES = 10_000
|
|
146
|
+
|
|
147
|
+
function pruneDirectorySizeCache(now = Date.now()) {
|
|
148
|
+
for (const [key, cached] of directorySizeCache) {
|
|
149
|
+
if (now - cached.ts >= DIRECTORY_SIZE_CACHE_TTL_MS) directorySizeCache.delete(key)
|
|
150
|
+
}
|
|
151
|
+
while (directorySizeCache.size > DIRECTORY_SIZE_CACHE_MAX_ENTRIES) {
|
|
152
|
+
const oldestKey = directorySizeCache.keys().next().value
|
|
153
|
+
if (oldestKey === undefined) break
|
|
154
|
+
directorySizeCache.delete(oldestKey)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
137
157
|
|
|
138
158
|
export async function directorySize(dir) {
|
|
139
159
|
try {
|
|
140
160
|
const now = Date.now()
|
|
141
161
|
const cached = directorySizeCache.get(dir)
|
|
142
162
|
if (cached && now - cached.ts < DIRECTORY_SIZE_CACHE_TTL_MS) return cached.size
|
|
163
|
+
if (cached) directorySizeCache.delete(dir)
|
|
143
164
|
|
|
144
165
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
145
166
|
const sizes = await Promise.all(entries.map(async (entry) => {
|
|
@@ -151,6 +172,7 @@ export async function directorySize(dir) {
|
|
|
151
172
|
}))
|
|
152
173
|
const size = sizes.reduce((sum, value) => sum + value, 0)
|
|
153
174
|
directorySizeCache.set(dir, { size, ts: now })
|
|
175
|
+
if (directorySizeCache.size > DIRECTORY_SIZE_CACHE_MAX_ENTRIES) pruneDirectorySizeCache(now)
|
|
154
176
|
return size
|
|
155
177
|
} catch {
|
|
156
178
|
return 0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{E as t,Ft as n,Mt as r,Ot as i,a,c as o,it as ee}from"./icons-pPRMD2tE.js";import{i as s,n as c}from"./react-vendor-CLbWF1Oy.js";import{$ as te,Q as l,V as ne,Z as re,et as ie,lt as u,mt as d,st as f,tt as ae}from"./index-BSXpUDCq.js";var p=e(n(),1),oe=s(),m=c(),h=144,g=82,_=4,v=8;function y(e){return JSON.stringify({provider:e.provider,modelId:e.id,api:e.api,baseUrl:e.baseUrl})}function b(e){if(!e)return{mode:`inherit`};try{let t=JSON.parse(e);return{mode:`fixed`,provider:String(t.provider||``),modelId:String(t.modelId||``),api:t.api?String(t.api):void 0,baseUrl:t.baseUrl?String(t.baseUrl):void 0}}catch{return{mode:`inherit`}}}function x(e){return!e||e.mode!==`fixed`?``:JSON.stringify({provider:e.provider,modelId:e.modelId,api:e.api,baseUrl:e.baseUrl})}function se(e){return e.name||`${e.provider}/${e.id}`}function S(){return{name:``,label:``,description:``,systemPrompt:``,allowedTools:[`read_file`,`grep_files`],maxRuntimeMs:`1800000`,maxToolCalls:`300`,enabledAsSubagent:!0,modelMode:`inherit`,fixedModelValue:``,thinkingLevel:`inherit`}}function ce(e){return{name:e.name,label:e.label,description:e.description??``,systemPrompt:e.systemPrompt??``,allowedTools:e.allowedTools??[],maxRuntimeMs:String(e.maxRuntimeMs??18e5),maxToolCalls:String(e.maxToolCalls??300),enabledAsSubagent:e.enabledAsSubagent,modelMode:e.model?.mode===`fixed`?`fixed`:`inherit`,fixedModelValue:x(e.model),thinkingLevel:e.thinkingLevel??`inherit`}}function le(e){return{name:e.name.trim().toLowerCase(),label:e.label.trim(),description:e.description.trim(),systemPrompt:e.systemPrompt.trim(),allowedTools:e.allowedTools,maxRuntimeMs:Number(e.maxRuntimeMs||18e5),maxToolCalls:Number(e.maxToolCalls||300),enabledAsSubagent:e.enabledAsSubagent,model:e.modelMode===`fixed`?b(e.fixedModelValue):{mode:`inherit`},thinkingLevel:e.thinkingLevel}}function C(e){return!!(e.name.trim()&&e.label.trim()&&e.allowedTools.length>0)}async function w(e,t){let n=await fetch(e,{...t,headers:{"content-type":`application/json`,...t?.headers}}),r=await n.json().catch(()=>null);if(!n.ok)throw Error(r?.error||`请求失败`);return r}function T(){let[e,n]=(0,p.useState)([]),[s,c]=(0,p.useState)([]),[x,T]=(0,p.useState)(!1),[E,D]=(0,p.useState)(null),[O,k]=(0,p.useState)(()=>S()),[A,j]=(0,p.useState)(!1),[M,N]=(0,p.useState)(``),[P,F]=(0,p.useState)(!1),[I,ue]=(0,p.useState)(),[L,de]=(0,p.useState)([]),[fe,pe]=(0,p.useState)(`off`),[R,z]=(0,p.useState)(``),[B,V]=(0,p.useState)(null),[H,U]=(0,p.useState)(null);async function W(){let[e,t]=await Promise.all([w(`/api/agent-profiles`),w(`/api/agent-profiles/available-tools`)]);n(e.agents),c(t.tools)}(0,p.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),c(r.tools)}catch(t){e||z(t instanceof Error?t.message:d(`requestFailed`))}}return t(),()=>{e=!0}},[]),(0,p.useEffect)(()=>{let e=!1;async function t(){try{let t=await te(),n=await l(t);de(n);let r=await ie(t),i=r.model??await ae(t)??n[0];if(e)return;ue(i),pe(r.thinkingLevel??re(i))}catch{}}return t(),()=>{e=!0}},[]),(0,p.useEffect)(()=>{if(!B)return;let e=()=>{V(null),U(null)},t=t=>{t.key===`Escape`&&e()};return window.addEventListener(`click`,e),window.addEventListener(`blur`,e),window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),document.addEventListener(`keydown`,t),()=>{window.removeEventListener(`click`,e),window.removeEventListener(`blur`,e),window.removeEventListener(`resize`,e),window.removeEventListener(`scroll`,e,!0),document.removeEventListener(`keydown`,t)}},[B]);let G=(0,p.useMemo)(()=>e.find(e=>e.id===E)??null,[e,E]),K=(0,p.useMemo)(()=>e.find(e=>e.id===B)??null,[e,B]),q=!!G?.readonly,J=!!(G?.readonly&&!G?.builtin),Y=(0,p.useMemo)(()=>L.find(e=>y(e)===O.fixedModelValue),[O.fixedModelValue,L]),X=O.modelMode===`fixed`&&!!Y&&Y?.reasoning!==!0;function me(e,t){if(e.stopPropagation(),B===t){V(null),U(null);return}let n=e.currentTarget.getBoundingClientRect(),r=Math.max(v,Math.min(n.right-h,window.innerWidth-h-v)),i=n.bottom+_,a=n.top-_-g;U({left:r,top:i+g<=window.innerHeight-v?i:Math.max(v,a)}),V(t)}function Z(e,t){k(n=>({...n,[e]:t}))}function he(e){k(t=>({...t,allowedTools:t.allowedTools.includes(e)?t.allowedTools.filter(t=>t!==e):[...t.allowedTools,e]}))}function ge(){D(null),k(S()),N(``),z(``),T(!0)}function Q(e){D(e.id),k(ce(e)),N(``),z(``),T(!0)}function $(){A||P||(T(!1),D(null),k(S()),N(``))}async function _e(){let e=M.trim();if(!e){z(d(`aiFillAgentInputRequired`));return}if(!I){z(d(`aiFillAgentNoModel`));return}F(!0),z(``);try{let t=await w(`/api/agent-profiles/ai-fill`,{method:`POST`,body:JSON.stringify({instruction:e,model:I,thinkingLevel:fe})});k(e=>({...e,name:t.agent.name,label:t.agent.label,description:t.agent.description,systemPrompt:t.agent.systemPrompt}))}catch(e){z(e instanceof Error?e.message:d(`aiFillAgentFailed`))}finally{F(!1)}}async function ve(){if(C(O)){j(!0),z(``);try{let e=G?.builtin?{model:O.modelMode===`fixed`?b(O.fixedModelValue):{mode:`inherit`}}:le({...O,thinkingLevel:X?`off`:O.thinkingLevel});E?await w(`/api/agent-profiles/${encodeURIComponent(E)}`,{method:`PATCH`,body:JSON.stringify(e)}):await w(`/api/agent-profiles`,{method:`POST`,body:JSON.stringify(e)}),$(),await W()}catch(e){z(e instanceof Error?e.message:d(`requestFailed`))}finally{j(!1)}}}async function ye(e){if(e.builtin||e.readonly)return;let t=!e.enabledAsSubagent,r=e.enabledAsSubagent;n(n=>n.map(n=>n.id===e.id?{...n,enabledAsSubagent:t}:n)),V(null);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`PATCH`,body:JSON.stringify({enabledAsSubagent:t})})}catch(t){n(t=>t.map(t=>t.id===e.id?{...t,enabledAsSubagent:r}:t)),z(t instanceof Error?t.message:d(`requestFailed`))}}async function be(e){if(!(e.builtin||e.readonly)&&await ne({description:d(`confirmDeleteAgent`),confirmLabel:d(`confirmDelete`),cancelLabel:d(`cancel`),variant:`destructive`})){z(``);try{await w(`/api/agent-profiles/${encodeURIComponent(e.id)}`,{method:`DELETE`}),await W()}catch(e){z(e instanceof Error?e.message:d(`requestFailed`))}}}return x?(0,m.jsxs)(`div`,{className:`quickforge-settings-stack`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-heading`,children:(0,m.jsxs)(`h3`,{className:`quickforge-settings-title`,children:[G?.builtin?d(`builtinAgentModelSettings`):d(G?`editAgent`:`createAgent`),(0,m.jsx)(f,{label:G?.builtin?d(`builtinAgentModelOnly`):G?.readonly?d(`readonlyAgentDescription`):d(`agentsDescription`)})]})}),(0,m.jsxs)(`section`,{className:`quickforge-settings-section`,"aria-label":d(G?`editAgent`:`createAgent`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`button`,{className:`quickforge-settings-button quickforge-settings-button-secondary`,type:`button`,onClick:$,disabled:A||P,children:[(0,m.jsx)(r,{className:`mr-2 size-4`}),d(`back`)]}),(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-row-title`,children:G?.builtin?d(`builtinAgentModelSettings`):d(G?`editAgent`:`createAgent`)}),G?.builtin?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:d(`builtinAgentModelOnly`)}):G?.readonly?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:d(`readonlyAgentDescription`)}):null]})]}),(0,m.jsx)(`div`,{className:`px-5 py-4`,children:(0,m.jsxs)(`div`,{className:`space-y-4`,children:[(0,m.jsxs)(`div`,{className:`rounded-2xl border border-border bg-muted/20 p-3`,children:[(0,m.jsxs)(`div`,{className:`mb-2 flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,m.jsx)(o,{className:`size-4 text-primary`}),d(`aiFillAgent`),(0,m.jsx)(f,{label:d(`aiFillAgentDescription`)})]}),(0,m.jsx)(`textarea`,{className:`min-h-20 w-full resize-y rounded-xl border border-input bg-background px-3 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground/65 focus:border-ring disabled:opacity-60`,value:M,disabled:q||P,onChange:e=>N(e.target.value),placeholder:d(`aiFillAgentPlaceholder`)}),(0,m.jsx)(`div`,{className:`mt-2 flex justify-end`,children:(0,m.jsxs)(u,{variant:`outline`,size:`sm`,onClick:()=>void _e(),disabled:q||P||!M.trim(),children:[(0,m.jsx)(o,{className:`mr-1 size-3.5`}),d(P?`aiFillAgentLoading`:`aiFillAgent`)]})})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentName`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.name,disabled:q,onChange:e=>Z(`name`,e.target.value),placeholder:`reviewer`})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentLabel`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.label,disabled:q,onChange:e=>Z(`label`,e.target.value),placeholder:d(`agentLabelPlaceholder`)})]})]}),G?(0,m.jsxs)(`div`,{className:`rounded-xl border border-border bg-muted/20 px-3 py-2 text-sm`,children:[(0,m.jsx)(`div`,{className:`text-xs font-medium text-muted-foreground`,children:d(`agentSourcePath`)}),(0,m.jsx)(`div`,{className:`mt-1 truncate font-mono text-xs text-foreground`,title:G.source?`${G.source}${G.relativePath?` · ${G.relativePath}`:``}`:void 0,children:G.source?`${G.source}${G.relativePath?` · ${G.relativePath}`:``}`:G.builtin?d(`builtinAgent`):`-`})]}):null,(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentDescription`),(0,m.jsx)(`input`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.description,disabled:q,onChange:e=>Z(`description`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentSystemPrompt`),(0,m.jsx)(`textarea`,{className:`mt-1 min-h-36 w-full resize-y rounded-xl border border-input bg-background px-3 py-2 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.systemPrompt,disabled:q,onChange:e=>Z(`systemPrompt`,e.target.value)})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentModelMode`),(0,m.jsxs)(`select`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.modelMode,disabled:J,onChange:e=>Z(`modelMode`,e.target.value),children:[(0,m.jsx)(`option`,{value:`inherit`,children:d(`agentModelInherit`)}),(0,m.jsx)(`option`,{value:`fixed`,children:d(`agentModelFixed`)})]})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentFixedModel`),(0,m.jsxs)(`select`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.fixedModelValue,disabled:J||O.modelMode!==`fixed`,onChange:e=>Z(`fixedModelValue`,e.target.value),children:[(0,m.jsx)(`option`,{value:``,children:d(`agentModelInherit`)}),L.map(e=>(0,m.jsx)(`option`,{value:y(e),children:se(e)},y(e)))]})]})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`agentThinkingLevel`),(0,m.jsxs)(`select`,{className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:X&&O.thinkingLevel!==`inherit`?`off`:O.thinkingLevel,disabled:q||X,onChange:e=>Z(`thinkingLevel`,e.target.value),children:[(0,m.jsx)(`option`,{value:`inherit`,children:d(`agentThinkingInherit`)}),(0,m.jsx)(`option`,{value:`off`,children:d(`thinkingOff`)}),(0,m.jsx)(`option`,{value:`low`,children:d(`thinkingLow`)}),(0,m.jsx)(`option`,{value:`medium`,children:d(`thinkingMedium`)}),(0,m.jsx)(`option`,{value:`high`,children:d(`thinkingHigh`)}),(0,m.jsx)(`option`,{value:`xhigh`,children:d(`thinkingXHigh`)})]}),(0,m.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:d(X?`agentThinkingUnsupported`:`agentThinkingDescription`)})]}),(0,m.jsxs)(`div`,{children:[(0,m.jsx)(`div`,{className:`mb-2 text-sm font-medium text-foreground`,children:d(`allowedTools`)}),(0,m.jsx)(`div`,{className:`grid gap-2 sm:grid-cols-2`,children:s.map(e=>(0,m.jsxs)(`label`,{className:`flex items-start gap-2 rounded-xl border border-border bg-muted/20 p-3 text-sm disabled:opacity-60`,children:[(0,m.jsx)(`input`,{type:`checkbox`,className:`mt-1`,disabled:q,checked:O.allowedTools.includes(e.name),onChange:()=>he(e.name)}),(0,m.jsxs)(`span`,{children:[(0,m.jsx)(`span`,{className:`font-medium text-foreground`,children:e.label}),(0,m.jsx)(`span`,{className:`ml-2 font-mono text-xs text-muted-foreground`,children:e.name}),e.riskLevel===`dangerous`?(0,m.jsx)(`span`,{className:`ml-2 rounded-full bg-amber-500/10 px-2 py-0.5 text-xs text-amber-700`,children:d(`highRiskTool`)}):null,(0,m.jsx)(`span`,{className:`mt-1 block text-xs text-muted-foreground`,children:e.description})]})]},e.name))})]}),(0,m.jsxs)(`div`,{className:`grid gap-3 sm:grid-cols-2`,children:[(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`maxRuntimeMs`),(0,m.jsx)(`input`,{type:`number`,className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.maxRuntimeMs,disabled:q,onChange:e=>Z(`maxRuntimeMs`,e.target.value)})]}),(0,m.jsxs)(`label`,{className:`block text-sm font-medium text-foreground`,children:[d(`maxToolCalls`),(0,m.jsx)(`input`,{type:`number`,className:`mt-1 h-10 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring disabled:opacity-60`,value:O.maxToolCalls,disabled:q,onChange:e=>Z(`maxToolCalls`,e.target.value)})]})]}),(0,m.jsxs)(`label`,{className:`flex items-center gap-2 text-sm text-foreground`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:O.enabledAsSubagent,disabled:q,onChange:e=>Z(`enabledAsSubagent`,e.target.checked)}),d(`enabledAsSubagent`)]}),R?(0,m.jsx)(`div`,{className:`rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive`,children:R}):null]})}),(0,m.jsxs)(`div`,{className:`quickforge-settings-divider flex justify-end gap-2 px-5 py-4`,children:[(0,m.jsx)(u,{variant:`outline`,onClick:$,disabled:A||P,children:d(`cancel`)}),(0,m.jsx)(u,{onClick:ve,disabled:A||P||J||!G?.builtin&&!C(O)||O.modelMode===`fixed`&&!O.fixedModelValue,children:d(`save`)})]})]})]}):(0,m.jsxs)(`div`,{className:`quickforge-settings-stack`,children:[(0,m.jsxs)(`section`,{className:`quickforge-settings-section`,"aria-label":d(`agentsTab`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-title`,children:[(0,m.jsx)(i,{className:`size-4 text-primary`}),d(`agentsTab`)]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:d(`agentsDescription`)})]}),(0,m.jsx)(`button`,{className:`quickforge-settings-button quickforge-settings-button-primary`,type:`button`,onClick:ge,children:d(`createAgent`)})]}),R?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:R}):null,e.length===0?(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`loading`)}):e.map(e=>(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item quickforge-agent-profile-row`,role:`button`,tabIndex:0,onClick:()=>Q(e),onKeyDown:t=>{t.target===t.currentTarget&&(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),Q(e))},children:[(0,m.jsx)(`div`,{className:`quickforge-settings-list-item-main quickforge-agent-profile-row-main`,children:(0,m.jsxs)(`div`,{className:`quickforge-agent-profile-summary`,children:[(0,m.jsx)(`span`,{className:`quickforge-agent-profile-label`,title:e.label,children:e.label}),e.description?(0,m.jsx)(`span`,{className:`quickforge-agent-profile-description`,title:e.description,children:e.description}):null]})}),(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":e.builtin||e.readonly?`true`:`false`,title:e.enabledAsSubagent?d(`disableAsSubagent`):d(`enableAsSubagent`),children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:e.enabledAsSubagent,disabled:e.builtin||e.readonly,onChange:()=>void ye(e)}),(0,m.jsx)(`span`,{"aria-hidden":`true`})]}),(0,m.jsx)(`button`,{className:`quickforge-settings-icon-action`,type:`button`,onClick:t=>me(t,e.id),title:d(`moreActions`),"aria-label":d(`moreActions`),"aria-haspopup":`menu`,"aria-expanded":B===e.id,children:(0,m.jsx)(ee,{className:`size-4`})})]})]},e.id))]}),K&&H?(0,oe.createPortal)((0,m.jsxs)(`div`,{className:`fixed z-50 w-36 overflow-hidden rounded-xl border border-border bg-popover py-1 text-sm shadow-quickforge`,style:{left:H.left,top:H.top},role:`menu`,"aria-label":d(`moreActions`),onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`button`,{className:`flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50`,type:`button`,role:`menuitem`,disabled:K.readonly&&!K.builtin,onClick:()=>{V(null),U(null),Q(K)},children:[(0,m.jsx)(t,{className:`size-3.5`}),K.builtin?d(`builtinAgentModelSettings`):d(`editTask`)]}),(0,m.jsxs)(`button`,{className:`flex w-full items-center gap-2 px-3 py-2 text-left text-destructive hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50`,type:`button`,role:`menuitem`,disabled:K.builtin||K.readonly,onClick:()=>{V(null),U(null),be(K)},children:[(0,m.jsx)(a,{className:`size-3.5`}),d(`delete`)]})]}),document.body):null]})}export{T as AgentProfilesPage};
|