@swarmclawai/swarmclaw 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +3 -3
- package/src/app/api/s/[token]/raw/route.ts +1 -1
- package/src/app/home/page.tsx +8 -1
- package/src/app/missions/page.tsx +148 -2
- package/src/app/s/[token]/page.tsx +173 -48
- package/src/components/home/home-launchpad.tsx +49 -2
- package/src/components/missions/mission-template-install-dialog.tsx +33 -1
- package/src/components/quality/quality-workspace.tsx +31 -6
- package/src/lib/server/sharing/share-resolver.test.ts +129 -0
- package/src/lib/server/sharing/share-resolver.ts +48 -3
- package/electron-dist/main.js +0 -218
package/README.md
CHANGED
|
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.8.0 Highlights
|
|
403
|
+
|
|
404
|
+
Mission Command release: a bigger operator update that makes autonomous missions easier to launch, inspect, and share.
|
|
405
|
+
|
|
406
|
+
- **Mission Command launchpad.** The home launchpad now opens concrete mission starters for Release QA, Launch Sprint, Cost Audit, and Connector Smoke Test instead of dropping users into a generic mission list.
|
|
407
|
+
- **Deep-linked mission templates.** `/missions?template=<id>` opens the right starter template directly, and the template installer can create a mission-driver chat when no sessions exist yet.
|
|
408
|
+
- **Quality Center handoffs.** `/quality?tab=evals|approvals|runs` is shareable, and the Quality overview/Eval Lab can start a Release QA mission from current operator evidence.
|
|
409
|
+
- **Public mission reports.** Missions can mint, copy, and revoke public share links from the detail view. Shared pages render status, budgets, milestones, and generated reports using the existing allowlisted share resolver.
|
|
410
|
+
- **Safer share payloads.** Mission milestones now expose `summary` correctly in public HTML and raw markdown shares, with regression coverage in `npm run test:runtime`.
|
|
411
|
+
|
|
412
|
+
### v1.7.3 Highlights
|
|
413
|
+
|
|
414
|
+
Desktop packaging fix for Linux AppImage and deb builds.
|
|
415
|
+
|
|
416
|
+
- **Linux desktop native modules match Electron.** Packaged Linux builds now copy Electron-rebuilt native addons into the embedded Next standalone server, fixing the `better-sqlite3` Node ABI mismatch reported in [#65](https://github.com/swarmclawai/swarmclaw/issues/65).
|
|
417
|
+
- **Desktop packaging regression coverage.** The Electron `afterPack` hook now has a Linux standalone native-module sync test wired into `npm run test:cli`.
|
|
418
|
+
- **macOS desktop note.** macOS builds remain ad-hoc signed and not notarized in this release, so the existing Gatekeeper/quarantine workaround still applies until Developer ID signing is available.
|
|
419
|
+
|
|
402
420
|
### v1.7.2 Highlights
|
|
403
421
|
|
|
404
422
|
CLI provider usability follow-up for v1.7.0/v1.7.1. The expanded coding-agent roster is now easier to find, configure, and validate from onboarding and setup diagnostics.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
"lint:baseline": "node ./scripts/lint-baseline.mjs check",
|
|
85
85
|
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
|
|
86
86
|
"cli": "node ./bin/swarmclaw.js",
|
|
87
|
-
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
87
|
+
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
88
88
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
89
89
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
90
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
90
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
91
91
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
92
92
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
93
93
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -49,7 +49,7 @@ export async function GET(_req: Request, ctx: { params: Promise<{ token: string
|
|
|
49
49
|
if (payload.milestones.length > 0) {
|
|
50
50
|
lines.push('## Milestones', '')
|
|
51
51
|
for (const m of payload.milestones) {
|
|
52
|
-
lines.push(`- ${new Date(m.at).toISOString().slice(0, 19).replace('T', ' ')}: ${m.
|
|
52
|
+
lines.push(`- ${new Date(m.at).toISOString().slice(0, 19).replace('T', ' ')}: ${m.summary}`)
|
|
53
53
|
}
|
|
54
54
|
lines.push('')
|
|
55
55
|
}
|
package/src/app/home/page.tsx
CHANGED
|
@@ -266,6 +266,10 @@ export default function HomePage() {
|
|
|
266
266
|
router.push(DEFAULT_BUILDER_ROUTE)
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
const openMissionTemplate = (templateId: string) => {
|
|
270
|
+
router.push(`/missions?template=${encodeURIComponent(templateId)}`)
|
|
271
|
+
}
|
|
272
|
+
|
|
269
273
|
if (homeMode === 'launchpad') {
|
|
270
274
|
return (
|
|
271
275
|
<MainContent>
|
|
@@ -286,7 +290,10 @@ export default function HomePage() {
|
|
|
286
290
|
onRunEvalSuite={() => navigateTo('quality')}
|
|
287
291
|
onReviewApprovals={() => navigateTo('quality')}
|
|
288
292
|
onInspectFailedRuns={() => navigateTo('quality')}
|
|
289
|
-
onStartReleaseQaMission={() =>
|
|
293
|
+
onStartReleaseQaMission={() => openMissionTemplate('release-candidate-qa')}
|
|
294
|
+
onStartLaunchSprintMission={() => openMissionTemplate('launch-week-growth-sprint')}
|
|
295
|
+
onStartCostAuditMission={() => openMissionTemplate('agent-cost-audit')}
|
|
296
|
+
onStartConnectorSmokeMission={() => openMissionTemplate('connector-smoke-test')}
|
|
290
297
|
/>
|
|
291
298
|
</div>
|
|
292
299
|
</MainContent>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
4
5
|
import { api } from '@/lib/app/api-client'
|
|
5
6
|
import { MainContent } from '@/components/layout/main-content'
|
|
6
7
|
import { HintTip } from '@/components/shared/hint-tip'
|
|
@@ -15,6 +16,18 @@ import type { Mission, MissionReport, MissionEvent, MissionTemplate, Session } f
|
|
|
15
16
|
import { toast } from 'sonner'
|
|
16
17
|
|
|
17
18
|
const POLL_MS = 4_000
|
|
19
|
+
const RELEASE_QA_TEMPLATE_ID = 'release-candidate-qa'
|
|
20
|
+
|
|
21
|
+
interface ShareLink {
|
|
22
|
+
id: string
|
|
23
|
+
token: string
|
|
24
|
+
entityType: 'mission' | 'skill' | 'session'
|
|
25
|
+
entityId: string
|
|
26
|
+
label: string | null
|
|
27
|
+
createdAt: number
|
|
28
|
+
expiresAt: number | null
|
|
29
|
+
revokedAt: number | null
|
|
30
|
+
}
|
|
18
31
|
|
|
19
32
|
const STATUS_BADGE: Record<Mission['status'], { label: string; cls: string }> = {
|
|
20
33
|
draft: { label: 'Draft', cls: 'bg-white/[0.05] text-text-3' },
|
|
@@ -398,7 +411,71 @@ interface DetailProps {
|
|
|
398
411
|
|
|
399
412
|
function MissionDetail({ mission, reports, events, busy, onAction, onForceReport, onEdit }: DetailProps) {
|
|
400
413
|
const [selectedReport, setSelectedReport] = useState<MissionReport | null>(null)
|
|
414
|
+
const [shareLinks, setShareLinks] = useState<ShareLink[]>([])
|
|
415
|
+
const [shareBusy, setShareBusy] = useState<string | null>(null)
|
|
401
416
|
const wallclockCapMs = mission.budget.maxWallclockSec != null ? mission.budget.maxWallclockSec * 1000 : null
|
|
417
|
+
const activeShare = useMemo(
|
|
418
|
+
() => shareLinks.find((link) => !link.revokedAt && (!link.expiresAt || link.expiresAt > Date.now())) ?? null,
|
|
419
|
+
[shareLinks],
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
const loadShareLinks = useCallback(async () => {
|
|
423
|
+
try {
|
|
424
|
+
const links = await api<ShareLink[]>('GET', `/share?entityType=mission&entityId=${encodeURIComponent(mission.id)}`)
|
|
425
|
+
setShareLinks(Array.isArray(links) ? links : [])
|
|
426
|
+
} catch {
|
|
427
|
+
setShareLinks([])
|
|
428
|
+
}
|
|
429
|
+
}, [mission.id])
|
|
430
|
+
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
void loadShareLinks()
|
|
433
|
+
}, [loadShareLinks])
|
|
434
|
+
|
|
435
|
+
const shareUrl = activeShare && typeof window !== 'undefined'
|
|
436
|
+
? `${window.location.origin}/s/${activeShare.token}`
|
|
437
|
+
: ''
|
|
438
|
+
|
|
439
|
+
const createShareLink = useCallback(async () => {
|
|
440
|
+
setShareBusy('create')
|
|
441
|
+
try {
|
|
442
|
+
const link = await api<ShareLink>('POST', '/share', {
|
|
443
|
+
entityType: 'mission',
|
|
444
|
+
entityId: mission.id,
|
|
445
|
+
label: `${mission.title} public report`,
|
|
446
|
+
})
|
|
447
|
+
setShareLinks((prev) => [link, ...prev])
|
|
448
|
+
toast.success('Mission share link created')
|
|
449
|
+
} catch (error) {
|
|
450
|
+
toast.error(error instanceof Error ? error.message : 'Unable to create share link')
|
|
451
|
+
} finally {
|
|
452
|
+
setShareBusy(null)
|
|
453
|
+
}
|
|
454
|
+
}, [mission.id, mission.title])
|
|
455
|
+
|
|
456
|
+
const revokeShareLink = useCallback(async () => {
|
|
457
|
+
if (!activeShare) return
|
|
458
|
+
setShareBusy(activeShare.id)
|
|
459
|
+
try {
|
|
460
|
+
const revoked = await api<ShareLink>('DELETE', `/share/${activeShare.id}`)
|
|
461
|
+
setShareLinks((prev) => prev.map((link) => (link.id === revoked.id ? revoked : link)))
|
|
462
|
+
toast.success('Mission share link revoked')
|
|
463
|
+
} catch (error) {
|
|
464
|
+
toast.error(error instanceof Error ? error.message : 'Unable to revoke share link')
|
|
465
|
+
} finally {
|
|
466
|
+
setShareBusy(null)
|
|
467
|
+
}
|
|
468
|
+
}, [activeShare])
|
|
469
|
+
|
|
470
|
+
const copyShareUrl = useCallback(async () => {
|
|
471
|
+
if (!shareUrl) return
|
|
472
|
+
try {
|
|
473
|
+
await navigator.clipboard.writeText(shareUrl)
|
|
474
|
+
toast.success('Share URL copied')
|
|
475
|
+
} catch {
|
|
476
|
+
toast.error('Unable to copy share URL')
|
|
477
|
+
}
|
|
478
|
+
}, [shareUrl])
|
|
402
479
|
|
|
403
480
|
return (
|
|
404
481
|
<div className="flex flex-col gap-4 p-4">
|
|
@@ -428,6 +505,53 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
|
|
|
428
505
|
<MissionControls mission={mission} onAction={onAction} onForceReport={onForceReport} onEdit={onEdit} busy={busy} />
|
|
429
506
|
</div>
|
|
430
507
|
|
|
508
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.025] p-4">
|
|
509
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
510
|
+
<div>
|
|
511
|
+
<div className="text-[11px] font-600 uppercase tracking-wide text-text-3">Public share</div>
|
|
512
|
+
<p className="mt-1 max-w-[620px] text-[12px] leading-relaxed text-text-3/70">
|
|
513
|
+
Publish a revocable mission artifact with status, budgets, milestones, and generated reports. Secrets, credentials, private files, and hidden runtime metadata stay out of the payload.
|
|
514
|
+
</p>
|
|
515
|
+
</div>
|
|
516
|
+
<div className="flex shrink-0 flex-wrap gap-2">
|
|
517
|
+
{activeShare ? (
|
|
518
|
+
<>
|
|
519
|
+
<button
|
|
520
|
+
type="button"
|
|
521
|
+
onClick={() => void copyShareUrl()}
|
|
522
|
+
className="rounded-[9px] border border-emerald-500/25 bg-emerald-500/10 px-2.5 py-1.5 text-[11px] font-700 text-emerald-200 hover:bg-emerald-500/15"
|
|
523
|
+
>
|
|
524
|
+
Copy link
|
|
525
|
+
</button>
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
528
|
+
disabled={!!shareBusy}
|
|
529
|
+
onClick={() => void revokeShareLink()}
|
|
530
|
+
className="rounded-[9px] border border-rose-500/20 bg-rose-500/[0.06] px-2.5 py-1.5 text-[11px] font-700 text-rose-200 hover:bg-rose-500/[0.1] disabled:opacity-40"
|
|
531
|
+
>
|
|
532
|
+
{shareBusy === activeShare.id ? 'Revoking...' : 'Revoke'}
|
|
533
|
+
</button>
|
|
534
|
+
</>
|
|
535
|
+
) : (
|
|
536
|
+
<button
|
|
537
|
+
type="button"
|
|
538
|
+
disabled={!!shareBusy}
|
|
539
|
+
onClick={() => void createShareLink()}
|
|
540
|
+
className="rounded-[9px] border border-emerald-500/30 bg-emerald-500/10 px-2.5 py-1.5 text-[11px] font-700 text-emerald-200 hover:bg-emerald-500/15 disabled:opacity-40"
|
|
541
|
+
>
|
|
542
|
+
{shareBusy === 'create' ? 'Creating...' : 'Create share link'}
|
|
543
|
+
</button>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
{activeShare && (
|
|
548
|
+
<div className="mt-3 rounded-[10px] border border-white/[0.06] bg-black/20 px-3 py-2 text-[11px] text-text-3">
|
|
549
|
+
<span className="font-mono text-text">{shareUrl}</span>
|
|
550
|
+
<span className="ml-2 text-text-3/55">Created {formatTimestamp(activeShare.createdAt)}</span>
|
|
551
|
+
</div>
|
|
552
|
+
)}
|
|
553
|
+
</div>
|
|
554
|
+
|
|
431
555
|
{mission.successCriteria.length > 0 && (
|
|
432
556
|
<div>
|
|
433
557
|
<div className="text-[11px] font-600 uppercase tracking-wide text-text-3 mb-2">Success criteria</div>
|
|
@@ -503,6 +627,8 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
|
|
|
503
627
|
}
|
|
504
628
|
|
|
505
629
|
export default function MissionsPage() {
|
|
630
|
+
const router = useRouter()
|
|
631
|
+
const searchParams = useSearchParams()
|
|
506
632
|
const [missions, setMissions] = useState<Mission[]>([])
|
|
507
633
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
508
634
|
const [reports, setReports] = useState<MissionReport[]>([])
|
|
@@ -570,6 +696,16 @@ export default function MissionsPage() {
|
|
|
570
696
|
return () => { cancelled = true }
|
|
571
697
|
}, [])
|
|
572
698
|
|
|
699
|
+
useEffect(() => {
|
|
700
|
+
const templateId = searchParams.get('template')?.trim()
|
|
701
|
+
if (!templateId || templates.length === 0) return
|
|
702
|
+
const template = templates.find((item) => item.id === templateId)
|
|
703
|
+
if (!template) return
|
|
704
|
+
setGalleryOpen(false)
|
|
705
|
+
setInstallTemplate(template)
|
|
706
|
+
router.replace('/missions', { scroll: false })
|
|
707
|
+
}, [router, searchParams, templates])
|
|
708
|
+
|
|
573
709
|
const handleAction = useCallback(async (action: string, reason?: string) => {
|
|
574
710
|
if (!selectedId) return
|
|
575
711
|
setBusy(true)
|
|
@@ -633,10 +769,17 @@ export default function MissionsPage() {
|
|
|
633
769
|
<div className="text-[10px] text-text-3">Autonomous goal-driven runs</div>
|
|
634
770
|
</div>
|
|
635
771
|
<button
|
|
636
|
-
onClick={() =>
|
|
772
|
+
onClick={() => {
|
|
773
|
+
const template = templates.find((item) => item.id === RELEASE_QA_TEMPLATE_ID)
|
|
774
|
+
if (template) {
|
|
775
|
+
setInstallTemplate(template)
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
setCreateOpen(true)
|
|
779
|
+
}}
|
|
637
780
|
className="text-[11px] font-600 px-2.5 py-1 rounded border border-emerald-500/30 bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/15"
|
|
638
781
|
>
|
|
639
|
-
+
|
|
782
|
+
+ Mission
|
|
640
783
|
</button>
|
|
641
784
|
</div>
|
|
642
785
|
{templates.length > 0 && (
|
|
@@ -736,6 +879,9 @@ export default function MissionsPage() {
|
|
|
736
879
|
sessions={sessions}
|
|
737
880
|
onClose={() => setInstallTemplate(null)}
|
|
738
881
|
onInstall={handleInstallTemplate}
|
|
882
|
+
onSessionCreated={(session) => {
|
|
883
|
+
setSessions((prev) => [session, ...prev.filter((item) => item.id !== session.id)])
|
|
884
|
+
}}
|
|
739
885
|
/>
|
|
740
886
|
|
|
741
887
|
<MissionEditSheet
|
|
@@ -19,65 +19,190 @@ export default async function SharedEntityPage({
|
|
|
19
19
|
if (!payload) notFound()
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<h1 className="mt-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
<main className="min-h-screen bg-[#080a0f] text-white">
|
|
23
|
+
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8 px-5 py-8 sm:px-8 lg:px-10">
|
|
24
|
+
<header className="border-b border-white/10 pb-6">
|
|
25
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.16em] text-emerald-300/80">
|
|
26
|
+
SwarmClaw shared {payload.kind}
|
|
27
|
+
</div>
|
|
28
|
+
<h1 className="mt-3 max-w-3xl font-display text-[28px] font-700 tracking-normal text-white">
|
|
29
|
+
{link.label || payloadTitle(payload)}
|
|
30
|
+
</h1>
|
|
31
|
+
<p className="mt-3 max-w-2xl text-[13px] leading-relaxed text-white/60">
|
|
32
|
+
Public, revocable share link. Runtime secrets, credentials, private files, and hidden control metadata are omitted.
|
|
33
|
+
</p>
|
|
34
|
+
</header>
|
|
35
|
+
{renderBody(payload)}
|
|
36
|
+
<footer className="border-t border-white/10 pt-4 text-[11px] text-white/45">
|
|
37
|
+
Generated by SwarmClaw. This page only contains the entity fields explicitly allowed by the share resolver.
|
|
38
|
+
</footer>
|
|
39
|
+
</div>
|
|
40
|
+
</main>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function payloadTitle(payload: SharedPayload): string {
|
|
45
|
+
if (payload.kind === 'mission') return payload.title
|
|
46
|
+
if (payload.kind === 'skill') return payload.name
|
|
47
|
+
return payload.name
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatUsd(n: number): string {
|
|
51
|
+
return `$${n.toFixed(n < 0.01 ? 4 : 2)}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function formatNumber(n: number): string {
|
|
55
|
+
return Math.round(n).toLocaleString()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatDuration(ms: number): string {
|
|
59
|
+
if (!ms) return '0s'
|
|
60
|
+
if (ms < 1000) return `${ms}ms`
|
|
61
|
+
const sec = Math.round(ms / 1000)
|
|
62
|
+
if (sec < 60) return `${sec}s`
|
|
63
|
+
const min = Math.round(sec / 60)
|
|
64
|
+
if (min < 60) return `${min}m`
|
|
65
|
+
if (min < 60 * 24) return `${Math.round((min / 60) * 10) / 10}h`
|
|
66
|
+
return `${Math.round((min / 1440) * 10) / 10}d`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Stat({ label, value, hint }: { label: string; value: string; hint?: string }) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="rounded-[12px] border border-white/10 bg-white/[0.035] px-4 py-3">
|
|
72
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.12em] text-white/45">{label}</div>
|
|
73
|
+
<div className="mt-2 font-display text-[22px] font-700 tracking-normal text-white">{value}</div>
|
|
74
|
+
{hint ? <div className="mt-1 text-[11px] leading-relaxed text-white/45">{hint}</div> : null}
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ReportMarkdown({ content }: { content: string }) {
|
|
80
|
+
const lines = content.split('\n')
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="text-[13px] leading-relaxed text-white/72">
|
|
84
|
+
{lines.map((line, index) => renderReportLine(line, index))}
|
|
35
85
|
</div>
|
|
36
86
|
)
|
|
37
87
|
}
|
|
38
88
|
|
|
89
|
+
function parseNumberedListItem(line: string): { marker: string; text: string } | null {
|
|
90
|
+
const dotIndex = line.indexOf('. ')
|
|
91
|
+
if (dotIndex <= 0 || dotIndex > 3) return null
|
|
92
|
+
for (let i = 0; i < dotIndex; i += 1) {
|
|
93
|
+
const code = line.charCodeAt(i)
|
|
94
|
+
if (code < 48 || code > 57) return null
|
|
95
|
+
}
|
|
96
|
+
return { marker: line.slice(0, dotIndex + 1), text: line.slice(dotIndex + 2) }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderReportLine(line: string, index: number) {
|
|
100
|
+
const trimmed = line.trim()
|
|
101
|
+
if (!trimmed) return <div key={index} className="h-2" />
|
|
102
|
+
if (trimmed === '---') return <hr key={index} className="my-4 border-white/10" />
|
|
103
|
+
if (trimmed.startsWith('### ')) {
|
|
104
|
+
return <h5 key={index} className="mb-2 mt-4 text-[13px] font-800 text-white">{trimmed.slice(4)}</h5>
|
|
105
|
+
}
|
|
106
|
+
if (trimmed.startsWith('## ')) {
|
|
107
|
+
return <h4 key={index} className="mb-2 mt-5 text-[13px] font-800 uppercase tracking-[0.1em] text-white/70">{trimmed.slice(3)}</h4>
|
|
108
|
+
}
|
|
109
|
+
if (trimmed.startsWith('# ')) {
|
|
110
|
+
return <h3 key={index} className="mb-3 mt-1 font-display text-[18px] font-700 text-white">{trimmed.slice(2)}</h3>
|
|
111
|
+
}
|
|
112
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
113
|
+
return (
|
|
114
|
+
<div key={index} className="my-1 flex gap-2">
|
|
115
|
+
<span className="text-white/40">•</span>
|
|
116
|
+
<span>{trimmed.slice(2)}</span>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
const numbered = parseNumberedListItem(trimmed)
|
|
121
|
+
if (numbered) {
|
|
122
|
+
return (
|
|
123
|
+
<div key={index} className="my-1 flex gap-2">
|
|
124
|
+
<span className="min-w-5 text-white/40">{numbered.marker}</span>
|
|
125
|
+
<span>{numbered.text}</span>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
return <p key={index} className="my-2">{trimmed}</p>
|
|
130
|
+
}
|
|
131
|
+
|
|
39
132
|
function renderBody(payload: SharedPayload) {
|
|
40
133
|
if (payload.kind === 'mission') {
|
|
134
|
+
const statusTone =
|
|
135
|
+
payload.status === 'running'
|
|
136
|
+
? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
|
|
137
|
+
: payload.status === 'failed' || payload.status === 'budget_exhausted'
|
|
138
|
+
? 'border-rose-400/25 bg-rose-400/10 text-rose-200'
|
|
139
|
+
: 'border-white/10 bg-white/[0.04] text-white/70'
|
|
41
140
|
return (
|
|
42
|
-
<section>
|
|
43
|
-
<
|
|
44
|
-
|
|
141
|
+
<section className="flex flex-col gap-6">
|
|
142
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
143
|
+
<div className="max-w-3xl">
|
|
144
|
+
<div className={`inline-flex rounded-full border px-2.5 py-1 text-[11px] font-800 uppercase tracking-[0.1em] ${statusTone}`}>
|
|
145
|
+
{payload.status.replaceAll('_', ' ')}
|
|
146
|
+
</div>
|
|
147
|
+
<h2 className="mt-4 font-display text-[24px] font-700 tracking-normal text-white">{payload.title}</h2>
|
|
148
|
+
<p className="mt-3 whitespace-pre-wrap text-[14px] leading-relaxed text-white/68">{payload.goal}</p>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="grid min-w-[280px] grid-cols-2 gap-2">
|
|
151
|
+
<Stat label="Turns" value={formatNumber(payload.usage.turnsRun)} hint={payload.budget.maxTurns != null ? `cap ${formatNumber(payload.budget.maxTurns)}` : 'no cap'} />
|
|
152
|
+
<Stat label="Spend" value={formatUsd(payload.usage.usdSpent)} hint={payload.budget.maxUsd != null ? `cap ${formatUsd(payload.budget.maxUsd)}` : 'no cap'} />
|
|
153
|
+
<Stat label="Tokens" value={formatNumber(payload.usage.tokensUsed)} hint={payload.budget.maxTokens != null ? `cap ${formatNumber(payload.budget.maxTokens)}` : 'no cap'} />
|
|
154
|
+
<Stat label="Wallclock" value={formatDuration(payload.usage.wallclockMsElapsed)} hint={payload.budget.maxWallclockSec != null ? `cap ${formatDuration(payload.budget.maxWallclockSec * 1000)}` : 'no cap'} />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
45
157
|
{payload.successCriteria.length > 0 ? (
|
|
46
|
-
|
|
47
|
-
<h3 className="
|
|
48
|
-
<ul className="mt-
|
|
158
|
+
<div className="rounded-[14px] border border-white/10 bg-white/[0.025] p-4">
|
|
159
|
+
<h3 className="text-[12px] font-800 uppercase tracking-[0.12em] text-white/55">Success criteria</h3>
|
|
160
|
+
<ul className="mt-3 grid gap-2 text-[13px] text-white/70 md:grid-cols-2">
|
|
49
161
|
{payload.successCriteria.map((c, i) => (
|
|
50
|
-
<li key={i}>{c}</li>
|
|
162
|
+
<li key={i} className="rounded-[10px] border border-white/10 bg-white/[0.025] px-3 py-2">{c}</li>
|
|
51
163
|
))}
|
|
52
164
|
</ul>
|
|
53
|
-
|
|
165
|
+
</div>
|
|
166
|
+
) : null}
|
|
167
|
+
{payload.latestReport ? (
|
|
168
|
+
<article className="rounded-[14px] border border-emerald-400/20 bg-emerald-400/[0.04] p-4">
|
|
169
|
+
<div className="text-[11px] font-800 uppercase tracking-[0.12em] text-emerald-200/80">Latest report</div>
|
|
170
|
+
<div className="mt-1 text-[15px] font-800 text-white">{payload.latestReport.title}</div>
|
|
171
|
+
<div className="mt-1 text-[11px] text-white/45">{formatTime(payload.latestReport.at)} - {payload.latestReport.format}</div>
|
|
172
|
+
<div className="mt-4 rounded-[12px] border border-white/10 bg-black/20 px-4 py-3">
|
|
173
|
+
<ReportMarkdown content={payload.latestReport.content} />
|
|
174
|
+
</div>
|
|
175
|
+
</article>
|
|
54
176
|
) : null}
|
|
55
177
|
{payload.milestones.length > 0 ? (
|
|
56
|
-
|
|
57
|
-
<h3 className="
|
|
58
|
-
<ol className="mt-
|
|
178
|
+
<div className="rounded-[14px] border border-white/10 bg-white/[0.025] p-4">
|
|
179
|
+
<h3 className="text-[12px] font-800 uppercase tracking-[0.12em] text-white/55">Milestones</h3>
|
|
180
|
+
<ol className="mt-3 space-y-2">
|
|
59
181
|
{payload.milestones.map((m, i) => (
|
|
60
|
-
<li key={i}>
|
|
61
|
-
<span className="text-
|
|
182
|
+
<li key={i} className="flex gap-3 rounded-[10px] border border-white/10 bg-white/[0.02] px-3 py-2 text-[12px]">
|
|
183
|
+
<span className="shrink-0 text-white/40">{formatTime(m.at)}</span>
|
|
184
|
+
<span className="shrink-0 font-800 text-white/55">{m.kind}</span>
|
|
185
|
+
<span className="text-white/75">{m.summary}</span>
|
|
62
186
|
</li>
|
|
63
187
|
))}
|
|
64
188
|
</ol>
|
|
65
|
-
|
|
189
|
+
</div>
|
|
66
190
|
) : null}
|
|
67
|
-
{payload.reports.length >
|
|
68
|
-
|
|
69
|
-
<h3 className="
|
|
70
|
-
<div className="mt-3
|
|
71
|
-
{payload.reports.map((r
|
|
72
|
-
<article key={
|
|
73
|
-
<div className="
|
|
74
|
-
|
|
191
|
+
{payload.reports.length > 1 ? (
|
|
192
|
+
<div className="rounded-[14px] border border-white/10 bg-white/[0.025] p-4">
|
|
193
|
+
<h3 className="text-[12px] font-800 uppercase tracking-[0.12em] text-white/55">Earlier reports</h3>
|
|
194
|
+
<div className="mt-3 grid gap-3">
|
|
195
|
+
{payload.reports.slice(1).map((r) => (
|
|
196
|
+
<article key={r.id} className="rounded-[12px] border border-white/10 bg-white/[0.02] p-4">
|
|
197
|
+
<div className="text-[13px] font-800 text-white">{r.title}</div>
|
|
198
|
+
<div className="mt-1 text-[11px] text-white/45">{formatTime(r.at)} - {r.format}</div>
|
|
199
|
+
<div className="mt-3">
|
|
200
|
+
<ReportMarkdown content={r.content} />
|
|
75
201
|
</div>
|
|
76
|
-
<pre className="whitespace-pre-wrap text-sm text-neutral-800">{r.content}</pre>
|
|
77
202
|
</article>
|
|
78
203
|
))}
|
|
79
204
|
</div>
|
|
80
|
-
|
|
205
|
+
</div>
|
|
81
206
|
) : null}
|
|
82
207
|
</section>
|
|
83
208
|
)
|
|
@@ -85,19 +210,19 @@ function renderBody(payload: SharedPayload) {
|
|
|
85
210
|
|
|
86
211
|
if (payload.kind === 'skill') {
|
|
87
212
|
return (
|
|
88
|
-
<section>
|
|
89
|
-
<h2 className="text-
|
|
213
|
+
<section className="rounded-[14px] border border-white/10 bg-white/[0.025] p-5">
|
|
214
|
+
<h2 className="font-display text-[22px] font-700 text-white">{payload.name}</h2>
|
|
90
215
|
{payload.tags.length > 0 ? (
|
|
91
216
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
92
217
|
{payload.tags.map((t) => (
|
|
93
|
-
<span key={t} className="rounded bg-
|
|
218
|
+
<span key={t} className="rounded bg-white/10 px-2 py-0.5 text-xs text-white/70">
|
|
94
219
|
{t}
|
|
95
220
|
</span>
|
|
96
221
|
))}
|
|
97
222
|
</div>
|
|
98
223
|
) : null}
|
|
99
|
-
<p className="mt-4 text-
|
|
100
|
-
<pre className="mt-6 whitespace-pre-wrap rounded border border-
|
|
224
|
+
<p className="mt-4 text-[13px] leading-relaxed text-white/65">{payload.description}</p>
|
|
225
|
+
<pre className="mt-6 whitespace-pre-wrap rounded-[12px] border border-white/10 bg-black/20 p-4 text-sm text-white/70">
|
|
101
226
|
{payload.content}
|
|
102
227
|
</pre>
|
|
103
228
|
</section>
|
|
@@ -105,22 +230,22 @@ function renderBody(payload: SharedPayload) {
|
|
|
105
230
|
}
|
|
106
231
|
|
|
107
232
|
return (
|
|
108
|
-
<section>
|
|
109
|
-
<h2 className="text-
|
|
233
|
+
<section className="rounded-[14px] border border-white/10 bg-white/[0.025] p-5">
|
|
234
|
+
<h2 className="font-display text-[22px] font-700 text-white">{payload.name}</h2>
|
|
110
235
|
{payload.agentName ? (
|
|
111
|
-
<div className="mt-1 text-sm text-
|
|
236
|
+
<div className="mt-1 text-sm text-white/50">Agent: {payload.agentName}</div>
|
|
112
237
|
) : null}
|
|
113
238
|
<div className="mt-6 space-y-4">
|
|
114
239
|
{payload.messages.map((m, i) => (
|
|
115
240
|
<article
|
|
116
241
|
key={i}
|
|
117
|
-
className="rounded border border-
|
|
242
|
+
className="rounded-[12px] border border-white/10 bg-white/[0.02] p-4"
|
|
118
243
|
>
|
|
119
|
-
<div className="mb-1 flex items-center justify-between text-xs text-
|
|
244
|
+
<div className="mb-1 flex items-center justify-between text-xs text-white/45">
|
|
120
245
|
<span className="uppercase">{m.role}</span>
|
|
121
246
|
{m.at ? <span>{formatTime(m.at)}</span> : null}
|
|
122
247
|
</div>
|
|
123
|
-
<pre className="whitespace-pre-wrap text-sm text-
|
|
248
|
+
<pre className="whitespace-pre-wrap text-sm text-white/70">{m.text}</pre>
|
|
124
249
|
</article>
|
|
125
250
|
))}
|
|
126
251
|
</div>
|
|
@@ -73,6 +73,9 @@ type Props = {
|
|
|
73
73
|
onReviewApprovals: () => void
|
|
74
74
|
onInspectFailedRuns: () => void
|
|
75
75
|
onStartReleaseQaMission: () => void
|
|
76
|
+
onStartLaunchSprintMission: () => void
|
|
77
|
+
onStartCostAuditMission: () => void
|
|
78
|
+
onStartConnectorSmokeMission: () => void
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export function HomeLaunchpad({
|
|
@@ -92,12 +95,15 @@ export function HomeLaunchpad({
|
|
|
92
95
|
onReviewApprovals,
|
|
93
96
|
onInspectFailedRuns,
|
|
94
97
|
onStartReleaseQaMission,
|
|
98
|
+
onStartLaunchSprintMission,
|
|
99
|
+
onStartCostAuditMission,
|
|
100
|
+
onStartConnectorSmokeMission,
|
|
95
101
|
}: Props) {
|
|
96
102
|
return (
|
|
97
103
|
<div className="max-w-[980px] mx-auto px-6 py-10">
|
|
98
104
|
<div className="rounded-[20px] border border-white/[0.06] bg-white/[0.025] p-6">
|
|
99
105
|
<div className="inline-flex rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.16em] text-text-3/70">
|
|
100
|
-
|
|
106
|
+
Mission Command
|
|
101
107
|
</div>
|
|
102
108
|
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
103
109
|
<div className="max-w-[620px]">
|
|
@@ -159,13 +165,54 @@ export function HomeLaunchpad({
|
|
|
159
165
|
kicker="Autonomous mission"
|
|
160
166
|
title="Run with budgets"
|
|
161
167
|
description="Start a mission template for release QA, research, support triage, cost audit, or failed-run review with reports and caps."
|
|
162
|
-
primaryLabel="
|
|
168
|
+
primaryLabel="Release QA"
|
|
163
169
|
secondaryLabel="Quality Center"
|
|
164
170
|
onPrimary={onStartReleaseQaMission}
|
|
165
171
|
onSecondary={onRunEvalSuite}
|
|
166
172
|
/>
|
|
167
173
|
</div>
|
|
168
174
|
|
|
175
|
+
<div className="mt-6 rounded-[18px] border border-white/[0.06] bg-white/[0.025] p-4">
|
|
176
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
177
|
+
<div>
|
|
178
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Mission starters</div>
|
|
179
|
+
<p className="mt-1 max-w-[620px] text-[12px] leading-relaxed text-text-3/68">
|
|
180
|
+
Jump directly into the workflows that produce reusable evidence and shareable reports.
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex flex-wrap gap-2">
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={onStartReleaseQaMission}
|
|
187
|
+
className="rounded-[10px] border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-[12px] font-display font-700 text-emerald-200 hover:bg-emerald-500/15"
|
|
188
|
+
>
|
|
189
|
+
Release QA
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={onStartLaunchSprintMission}
|
|
194
|
+
className="rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-display font-700 text-text-2 hover:bg-white/[0.08]"
|
|
195
|
+
>
|
|
196
|
+
Launch Sprint
|
|
197
|
+
</button>
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={onStartCostAuditMission}
|
|
201
|
+
className="rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-display font-700 text-text-2 hover:bg-white/[0.08]"
|
|
202
|
+
>
|
|
203
|
+
Cost Audit
|
|
204
|
+
</button>
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={onStartConnectorSmokeMission}
|
|
208
|
+
className="rounded-[10px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-display font-700 text-text-2 hover:bg-white/[0.08]"
|
|
209
|
+
>
|
|
210
|
+
Connector Smoke
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
169
216
|
<div className="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
170
217
|
<LaunchActionCard
|
|
171
218
|
title={firstAgent ? 'Open First Agent Chat' : 'Open Agents'}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/app/api-client'
|
|
4
5
|
import { HintTip } from '@/components/shared/hint-tip'
|
|
5
6
|
import { AdvancedSettingsSection } from '@/components/shared/advanced-settings-section'
|
|
6
7
|
import { inputClass } from '@/components/shared/form-styles'
|
|
@@ -28,6 +29,7 @@ interface Props {
|
|
|
28
29
|
sessions: Session[]
|
|
29
30
|
onClose: () => void
|
|
30
31
|
onInstall: (template: MissionTemplate, input: InstantiateInput) => Promise<void>
|
|
32
|
+
onSessionCreated?: (session: Session) => void
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function formatDuration(sec: number | null | undefined): string {
|
|
@@ -48,7 +50,7 @@ function intOrNull(s: string): number | null {
|
|
|
48
50
|
return n == null ? null : Math.round(n)
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export function MissionTemplateInstallDialog({ template, sessions, onClose, onInstall }: Props) {
|
|
53
|
+
export function MissionTemplateInstallDialog({ template, sessions, onClose, onInstall, onSessionCreated }: Props) {
|
|
52
54
|
const [title, setTitle] = useState('')
|
|
53
55
|
const [goal, setGoal] = useState('')
|
|
54
56
|
const [criteriaText, setCriteriaText] = useState('')
|
|
@@ -134,6 +136,26 @@ export function MissionTemplateInstallDialog({ template, sessions, onClose, onIn
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
const createDriverSession = async () => {
|
|
140
|
+
if (!template) return
|
|
141
|
+
setBusy(true)
|
|
142
|
+
try {
|
|
143
|
+
const session = await api<Session>('POST', '/chats', {
|
|
144
|
+
name: `${template.name} mission driver`,
|
|
145
|
+
sessionType: 'human',
|
|
146
|
+
heartbeatEnabled: true,
|
|
147
|
+
heartbeatIntervalSec: 300,
|
|
148
|
+
})
|
|
149
|
+
setRootSessionId(session.id)
|
|
150
|
+
onSessionCreated?.(session)
|
|
151
|
+
toast.success('Mission driver chat created')
|
|
152
|
+
} catch (error) {
|
|
153
|
+
toast.error(error instanceof Error ? error.message : 'Unable to create mission driver chat')
|
|
154
|
+
} finally {
|
|
155
|
+
setBusy(false)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
137
159
|
return (
|
|
138
160
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" onClick={onClose}>
|
|
139
161
|
<div
|
|
@@ -197,6 +219,16 @@ export function MissionTemplateInstallDialog({ template, sessions, onClose, onIn
|
|
|
197
219
|
</option>
|
|
198
220
|
))}
|
|
199
221
|
</select>
|
|
222
|
+
{sessions.length === 0 && (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={() => void createDriverSession()}
|
|
226
|
+
disabled={busy}
|
|
227
|
+
className="mt-2 self-start rounded-[10px] border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-[12px] font-700 text-emerald-200 hover:bg-emerald-500/15 disabled:opacity-40"
|
|
228
|
+
>
|
|
229
|
+
Create mission driver chat
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
200
232
|
</label>
|
|
201
233
|
</div>
|
|
202
234
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
4
5
|
import { toast } from 'sonner'
|
|
5
6
|
import { MainContent } from '@/components/layout/main-content'
|
|
6
7
|
import { RunList } from '@/components/runs/run-list'
|
|
@@ -104,6 +105,8 @@ function EmptyState({ title, description }: { title: string; description: string
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
export function QualityWorkspace() {
|
|
108
|
+
const router = useRouter()
|
|
109
|
+
const searchParams = useSearchParams()
|
|
107
110
|
const agents = useAppStore((s) => s.agents)
|
|
108
111
|
const agentOptions = useMemo(
|
|
109
112
|
() => Object.values(agents).filter((agent) => !agent.trashedAt),
|
|
@@ -125,6 +128,20 @@ export function QualityWorkspace() {
|
|
|
125
128
|
const [evalBusy, setEvalBusy] = useState<string | null>(null)
|
|
126
129
|
const [approvalBusy, setApprovalBusy] = useState<string | null>(null)
|
|
127
130
|
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const tab = searchParams.get('tab') as QualityTab | null
|
|
133
|
+
if (tab && TABS.some((item) => item.id === tab)) setActiveTab(tab)
|
|
134
|
+
}, [searchParams])
|
|
135
|
+
|
|
136
|
+
const selectTab = useCallback((tab: QualityTab) => {
|
|
137
|
+
setActiveTab(tab)
|
|
138
|
+
router.replace(`/quality?tab=${tab}`, { scroll: false })
|
|
139
|
+
}, [router])
|
|
140
|
+
|
|
141
|
+
const openMissionTemplate = useCallback((templateId: string) => {
|
|
142
|
+
router.push(`/missions?template=${encodeURIComponent(templateId)}`)
|
|
143
|
+
}, [router])
|
|
144
|
+
|
|
128
145
|
const loadQualityData = useCallback(async (opts: { silent?: boolean } = {}) => {
|
|
129
146
|
if (opts.silent) setRefreshing(true)
|
|
130
147
|
else setLoading(true)
|
|
@@ -278,7 +295,7 @@ export function QualityWorkspace() {
|
|
|
278
295
|
<button
|
|
279
296
|
key={tab.id}
|
|
280
297
|
type="button"
|
|
281
|
-
onClick={() =>
|
|
298
|
+
onClick={() => selectTab(tab.id)}
|
|
282
299
|
className={cn(
|
|
283
300
|
'min-w-fit rounded-[9px] px-3 py-2 text-[12px] font-700 transition-colors',
|
|
284
301
|
activeTab === tab.id
|
|
@@ -328,9 +345,10 @@ export function QualityWorkspace() {
|
|
|
328
345
|
<p className="mt-1 text-[12px] text-text-3/65">Shortest path to unblock operator review.</p>
|
|
329
346
|
</div>
|
|
330
347
|
<div className="flex flex-wrap gap-2">
|
|
331
|
-
<button onClick={() =>
|
|
332
|
-
<button onClick={() =>
|
|
333
|
-
<button onClick={() =>
|
|
348
|
+
<button onClick={() => openMissionTemplate('release-candidate-qa')} className="rounded-[9px] border border-emerald-500/25 bg-emerald-500/10 px-2.5 py-1.5 text-[11px] font-700 text-emerald-200 hover:bg-emerald-500/15">Start QA Mission</button>
|
|
349
|
+
<button onClick={() => selectTab('evals')} className="rounded-[9px] border border-white/[0.08] px-2.5 py-1.5 text-[11px] font-700 text-text-2 hover:bg-white/[0.05]">Eval Lab</button>
|
|
350
|
+
<button onClick={() => selectTab('approvals')} className="rounded-[9px] border border-white/[0.08] px-2.5 py-1.5 text-[11px] font-700 text-text-2 hover:bg-white/[0.05]">Approvals</button>
|
|
351
|
+
<button onClick={() => selectTab('runs')} className="rounded-[9px] border border-white/[0.08] px-2.5 py-1.5 text-[11px] font-700 text-text-2 hover:bg-white/[0.05]">Runs</button>
|
|
334
352
|
</div>
|
|
335
353
|
</div>
|
|
336
354
|
{runHealth.recentFailures.length === 0 && approvalGroups.totalPending === 0 && evalSummary.failedRuns === 0 ? (
|
|
@@ -340,7 +358,7 @@ export function QualityWorkspace() {
|
|
|
340
358
|
{runHealth.recentFailures.slice(0, 4).map((run) => (
|
|
341
359
|
<button
|
|
342
360
|
key={run.id}
|
|
343
|
-
onClick={() =>
|
|
361
|
+
onClick={() => selectTab('runs')}
|
|
344
362
|
className="rounded-[12px] border border-rose-500/20 bg-rose-500/[0.04] px-3 py-3 text-left transition-colors hover:bg-rose-500/[0.07]"
|
|
345
363
|
>
|
|
346
364
|
<div className="text-[11px] font-700 uppercase tracking-[0.1em] text-rose-300">Failed Run</div>
|
|
@@ -351,7 +369,7 @@ export function QualityWorkspace() {
|
|
|
351
369
|
{approvalGroups.categories.slice(0, 4).map((group) => (
|
|
352
370
|
<button
|
|
353
371
|
key={group.category}
|
|
354
|
-
onClick={() =>
|
|
372
|
+
onClick={() => selectTab('approvals')}
|
|
355
373
|
className="rounded-[12px] border border-amber-500/20 bg-amber-500/[0.04] px-3 py-3 text-left transition-colors hover:bg-amber-500/[0.07]"
|
|
356
374
|
>
|
|
357
375
|
<div className="text-[11px] font-700 uppercase tracking-[0.1em] text-amber-300">Approval</div>
|
|
@@ -435,6 +453,13 @@ export function QualityWorkspace() {
|
|
|
435
453
|
</div>
|
|
436
454
|
</div>
|
|
437
455
|
)}
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
onClick={() => openMissionTemplate('release-candidate-qa')}
|
|
459
|
+
className="mt-3 w-full rounded-[10px] border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-[12px] font-800 text-emerald-200 transition-colors hover:bg-emerald-500/15"
|
|
460
|
+
>
|
|
461
|
+
Start Release QA Mission
|
|
462
|
+
</button>
|
|
438
463
|
<button
|
|
439
464
|
type="button"
|
|
440
465
|
disabled={!selectedAgentId || !selectedScenarioId || !!evalBusy}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
import type { Mission, MissionReport } from '@/types'
|
|
8
|
+
|
|
9
|
+
const originalEnv = {
|
|
10
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
11
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
12
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let tempDir = ''
|
|
16
|
+
let repo: typeof import('@/lib/server/missions/mission-repository')
|
|
17
|
+
let resolver: typeof import('./share-resolver')
|
|
18
|
+
|
|
19
|
+
function makeMission(overrides: Partial<Mission> = {}): Mission {
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
return {
|
|
22
|
+
id: overrides.id ?? 'mi_share_1',
|
|
23
|
+
title: 'Shared mission',
|
|
24
|
+
goal: 'Produce a launch report',
|
|
25
|
+
successCriteria: ['Report exists', 'Evidence is cited'],
|
|
26
|
+
rootSessionId: 'share_session_1',
|
|
27
|
+
agentIds: ['agent_1'],
|
|
28
|
+
status: 'running',
|
|
29
|
+
budget: {
|
|
30
|
+
maxUsd: 2,
|
|
31
|
+
maxTokens: 100_000,
|
|
32
|
+
maxToolCalls: null,
|
|
33
|
+
maxWallclockSec: 86_400,
|
|
34
|
+
maxTurns: 120,
|
|
35
|
+
warnAtFractions: [0.5, 0.8, 0.95],
|
|
36
|
+
},
|
|
37
|
+
usage: {
|
|
38
|
+
usdSpent: 0.42,
|
|
39
|
+
tokensUsed: 12_345,
|
|
40
|
+
toolCallsUsed: 9,
|
|
41
|
+
turnsRun: 12,
|
|
42
|
+
wallclockMsElapsed: 900_000,
|
|
43
|
+
startedAt: now - 900_000,
|
|
44
|
+
lastUpdatedAt: now,
|
|
45
|
+
warnFractionsHit: [],
|
|
46
|
+
},
|
|
47
|
+
milestones: [
|
|
48
|
+
{
|
|
49
|
+
id: 'ms_1',
|
|
50
|
+
at: now - 1000,
|
|
51
|
+
kind: 'subgoal_done',
|
|
52
|
+
summary: 'Release evidence collected',
|
|
53
|
+
evidence: ['run_1'],
|
|
54
|
+
sessionId: 'share_session_1',
|
|
55
|
+
runId: 'run_1',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
reportSchedule: null,
|
|
59
|
+
reportConnectorIds: [],
|
|
60
|
+
createdAt: now - 1_000_000,
|
|
61
|
+
updatedAt: now,
|
|
62
|
+
...overrides,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeReport(missionId: string, overrides: Partial<MissionReport> = {}): MissionReport {
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
return {
|
|
69
|
+
id: overrides.id ?? 'mrep_share_1',
|
|
70
|
+
missionId,
|
|
71
|
+
generatedAt: overrides.generatedAt ?? now,
|
|
72
|
+
format: 'markdown',
|
|
73
|
+
fromAt: now - 10_000,
|
|
74
|
+
toAt: now,
|
|
75
|
+
title: overrides.title ?? 'Shared mission: progress update',
|
|
76
|
+
body: overrides.body ?? '# Shared mission\n\nEvidence is ready.',
|
|
77
|
+
deliveredTo: [],
|
|
78
|
+
highlights: [],
|
|
79
|
+
...overrides,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
before(async () => {
|
|
84
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-share-resolver-'))
|
|
85
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
86
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
87
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
88
|
+
repo = await import('@/lib/server/missions/mission-repository')
|
|
89
|
+
resolver = await import('./share-resolver')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
after(() => {
|
|
93
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
94
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
95
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
96
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
97
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
98
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
99
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('share-resolver', () => {
|
|
103
|
+
it('resolves a public mission payload with summary milestones and safe report metadata', () => {
|
|
104
|
+
const mission = makeMission({ id: 'mi_public_share' })
|
|
105
|
+
repo.upsertMission(mission)
|
|
106
|
+
repo.saveMissionReport(makeReport(mission.id, { id: 'mrep_old', generatedAt: mission.createdAt + 1000, title: 'Old report' }))
|
|
107
|
+
repo.saveMissionReport(makeReport(mission.id, { id: 'mrep_new', generatedAt: mission.createdAt + 2000, title: 'Latest report' }))
|
|
108
|
+
|
|
109
|
+
const payload = resolver.resolveSharedEntity({
|
|
110
|
+
id: 'share_1',
|
|
111
|
+
token: 'tok',
|
|
112
|
+
entityType: 'mission',
|
|
113
|
+
entityId: mission.id,
|
|
114
|
+
label: null,
|
|
115
|
+
createdAt: Date.now(),
|
|
116
|
+
expiresAt: null,
|
|
117
|
+
revokedAt: null,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
assert.equal(payload?.kind, 'mission')
|
|
121
|
+
if (payload?.kind !== 'mission') return
|
|
122
|
+
assert.equal(payload.milestones[0]?.summary, 'Release evidence collected')
|
|
123
|
+
assert.equal(Object.hasOwn(payload.milestones[0] ?? {}, 'note'), false)
|
|
124
|
+
assert.equal(payload.usage.turnsRun, 12)
|
|
125
|
+
assert.equal(payload.budget.maxUsd, 2)
|
|
126
|
+
assert.equal(payload.latestReport?.title, 'Latest report')
|
|
127
|
+
assert.deepEqual(payload.reports.map((r) => r.title), ['Latest report', 'Old report'])
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -10,8 +10,25 @@ export interface SharedMissionPayload {
|
|
|
10
10
|
successCriteria: string[]
|
|
11
11
|
status: string
|
|
12
12
|
createdAt: number
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
updatedAt: number | null
|
|
14
|
+
usage: {
|
|
15
|
+
usdSpent: number
|
|
16
|
+
tokensUsed: number
|
|
17
|
+
toolCallsUsed: number
|
|
18
|
+
turnsRun: number
|
|
19
|
+
wallclockMsElapsed: number
|
|
20
|
+
startedAt: number | null
|
|
21
|
+
}
|
|
22
|
+
budget: {
|
|
23
|
+
maxUsd: number | null
|
|
24
|
+
maxTokens: number | null
|
|
25
|
+
maxToolCalls: number | null
|
|
26
|
+
maxWallclockSec: number | null
|
|
27
|
+
maxTurns: number | null
|
|
28
|
+
}
|
|
29
|
+
milestones: Array<{ at: number; summary: string; kind: string; evidence: string[] }>
|
|
30
|
+
reports: Array<{ id: string; at: number; title: string; format: string; content: string }>
|
|
31
|
+
latestReport: { id: string; at: number; title: string; format: string; content: string } | null
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
export interface SharedSkillPayload {
|
|
@@ -56,6 +73,8 @@ export function resolveSharedEntity(link: ShareLink): SharedPayload | null {
|
|
|
56
73
|
function resolveMission(id: string): SharedMissionPayload | null {
|
|
57
74
|
const raw = loadStoredItem('agent_missions', id) as Record<string, unknown> | null
|
|
58
75
|
if (!raw) return null
|
|
76
|
+
const usageRaw = (raw.usage || {}) as Record<string, unknown>
|
|
77
|
+
const budgetRaw = (raw.budget || {}) as Record<string, unknown>
|
|
59
78
|
const milestonesRaw = Array.isArray(raw.milestones) ? raw.milestones : []
|
|
60
79
|
const milestones = milestonesRaw
|
|
61
80
|
.slice(-MAX_MILESTONES)
|
|
@@ -63,8 +82,15 @@ function resolveMission(id: string): SharedMissionPayload | null {
|
|
|
63
82
|
const entry = (m || {}) as Record<string, unknown>
|
|
64
83
|
return {
|
|
65
84
|
at: typeof entry.at === 'number' ? entry.at : 0,
|
|
66
|
-
|
|
85
|
+
summary: typeof entry.summary === 'string'
|
|
86
|
+
? entry.summary
|
|
87
|
+
: typeof entry.note === 'string'
|
|
88
|
+
? entry.note
|
|
89
|
+
: '',
|
|
67
90
|
kind: typeof entry.kind === 'string' ? entry.kind : 'note',
|
|
91
|
+
evidence: Array.isArray(entry.evidence)
|
|
92
|
+
? entry.evidence.filter((x): x is string => typeof x === 'string')
|
|
93
|
+
: [],
|
|
68
94
|
}
|
|
69
95
|
})
|
|
70
96
|
|
|
@@ -72,7 +98,9 @@ function resolveMission(id: string): SharedMissionPayload | null {
|
|
|
72
98
|
try {
|
|
73
99
|
const rows = listMissionReports(id, MAX_REPORTS)
|
|
74
100
|
reports = rows.map((r) => ({
|
|
101
|
+
id: r.id,
|
|
75
102
|
at: r.generatedAt,
|
|
103
|
+
title: r.title,
|
|
76
104
|
format: String(r.format),
|
|
77
105
|
content: r.body,
|
|
78
106
|
}))
|
|
@@ -90,8 +118,25 @@ function resolveMission(id: string): SharedMissionPayload | null {
|
|
|
90
118
|
: [],
|
|
91
119
|
status: typeof raw.status === 'string' ? raw.status : 'unknown',
|
|
92
120
|
createdAt: typeof raw.createdAt === 'number' ? raw.createdAt : 0,
|
|
121
|
+
updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : null,
|
|
122
|
+
usage: {
|
|
123
|
+
usdSpent: typeof usageRaw.usdSpent === 'number' ? usageRaw.usdSpent : 0,
|
|
124
|
+
tokensUsed: typeof usageRaw.tokensUsed === 'number' ? usageRaw.tokensUsed : 0,
|
|
125
|
+
toolCallsUsed: typeof usageRaw.toolCallsUsed === 'number' ? usageRaw.toolCallsUsed : 0,
|
|
126
|
+
turnsRun: typeof usageRaw.turnsRun === 'number' ? usageRaw.turnsRun : 0,
|
|
127
|
+
wallclockMsElapsed: typeof usageRaw.wallclockMsElapsed === 'number' ? usageRaw.wallclockMsElapsed : 0,
|
|
128
|
+
startedAt: typeof usageRaw.startedAt === 'number' ? usageRaw.startedAt : null,
|
|
129
|
+
},
|
|
130
|
+
budget: {
|
|
131
|
+
maxUsd: typeof budgetRaw.maxUsd === 'number' ? budgetRaw.maxUsd : null,
|
|
132
|
+
maxTokens: typeof budgetRaw.maxTokens === 'number' ? budgetRaw.maxTokens : null,
|
|
133
|
+
maxToolCalls: typeof budgetRaw.maxToolCalls === 'number' ? budgetRaw.maxToolCalls : null,
|
|
134
|
+
maxWallclockSec: typeof budgetRaw.maxWallclockSec === 'number' ? budgetRaw.maxWallclockSec : null,
|
|
135
|
+
maxTurns: typeof budgetRaw.maxTurns === 'number' ? budgetRaw.maxTurns : null,
|
|
136
|
+
},
|
|
93
137
|
milestones,
|
|
94
138
|
reports,
|
|
139
|
+
latestReport: reports[0] ?? null,
|
|
95
140
|
}
|
|
96
141
|
}
|
|
97
142
|
|
package/electron-dist/main.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
const electron_1 = require("electron");
|
|
40
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
-
const paths_1 = require("./paths");
|
|
43
|
-
const server_lifecycle_1 = require("./server-lifecycle");
|
|
44
|
-
const menu_1 = require("./menu");
|
|
45
|
-
const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
|
|
46
|
-
const LOG_TAIL_BYTES = 1500;
|
|
47
|
-
let mainWindow = null;
|
|
48
|
-
let serverHandle = null;
|
|
49
|
-
let serverLogFile = null;
|
|
50
|
-
let isQuitting = false;
|
|
51
|
-
const gotLock = electron_1.app.requestSingleInstanceLock();
|
|
52
|
-
if (!gotLock) {
|
|
53
|
-
electron_1.app.quit();
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
electron_1.app.on('second-instance', () => {
|
|
57
|
-
if (mainWindow) {
|
|
58
|
-
if (mainWindow.isMinimized())
|
|
59
|
-
mainWindow.restore();
|
|
60
|
-
mainWindow.focus();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
electron_1.app.on('ready', () => void onReady());
|
|
64
|
-
electron_1.app.on('window-all-closed', () => {
|
|
65
|
-
if (process.platform !== 'darwin')
|
|
66
|
-
electron_1.app.quit();
|
|
67
|
-
});
|
|
68
|
-
electron_1.app.on('activate', () => {
|
|
69
|
-
if (mainWindow !== null)
|
|
70
|
-
return;
|
|
71
|
-
if (serverHandle) {
|
|
72
|
-
createMainWindow(serverHandle.url);
|
|
73
|
-
}
|
|
74
|
-
else if (!electron_1.app.isPackaged) {
|
|
75
|
-
createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
electron_1.app.on('before-quit', () => {
|
|
79
|
-
isQuitting = true;
|
|
80
|
-
});
|
|
81
|
-
electron_1.app.on('will-quit', async (event) => {
|
|
82
|
-
if (!serverHandle)
|
|
83
|
-
return;
|
|
84
|
-
event.preventDefault();
|
|
85
|
-
try {
|
|
86
|
-
await serverHandle.stop();
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
serverHandle = null;
|
|
90
|
-
electron_1.app.exit(0);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
async function onReady() {
|
|
95
|
-
const paths = (0, paths_1.resolveRuntimePaths)();
|
|
96
|
-
(0, menu_1.buildAppMenu)(paths, () => mainWindow);
|
|
97
|
-
const iconPath = resolveIconPath();
|
|
98
|
-
if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
|
|
99
|
-
const img = electron_1.nativeImage.createFromPath(iconPath);
|
|
100
|
-
if (!img.isEmpty())
|
|
101
|
-
electron_1.app.dock.setIcon(img);
|
|
102
|
-
}
|
|
103
|
-
if (!electron_1.app.isPackaged) {
|
|
104
|
-
const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
|
|
105
|
-
console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
|
|
106
|
-
createMainWindow(devUrl);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
|
|
110
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
|
|
111
|
-
try {
|
|
112
|
-
serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
|
|
113
|
-
paths,
|
|
114
|
-
logFile: serverLogFile,
|
|
115
|
-
onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
|
|
116
|
-
onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
|
|
117
|
-
onExit: (code, signal) => {
|
|
118
|
-
if (!isQuitting) {
|
|
119
|
-
console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
|
|
120
|
-
void showServerCrashDialog(code, signal);
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
await showStartupFailureDialog(err, paths);
|
|
127
|
-
electron_1.app.exit(1);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
createMainWindow(serverHandle.url);
|
|
131
|
-
void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
|
|
132
|
-
}
|
|
133
|
-
function resolveIconPath() {
|
|
134
|
-
const candidate = electron_1.app.isPackaged
|
|
135
|
-
? node_path_1.default.join(process.resourcesPath, 'icon.png')
|
|
136
|
-
: node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
|
|
137
|
-
return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
|
|
138
|
-
}
|
|
139
|
-
function createMainWindow(startUrl) {
|
|
140
|
-
const iconPath = resolveIconPath();
|
|
141
|
-
mainWindow = new electron_1.BrowserWindow({
|
|
142
|
-
width: 1440,
|
|
143
|
-
height: 900,
|
|
144
|
-
minWidth: 1024,
|
|
145
|
-
minHeight: 640,
|
|
146
|
-
backgroundColor: '#0b0b0f',
|
|
147
|
-
show: true,
|
|
148
|
-
...(iconPath ? { icon: iconPath } : {}),
|
|
149
|
-
webPreferences: {
|
|
150
|
-
contextIsolation: true,
|
|
151
|
-
nodeIntegration: false,
|
|
152
|
-
sandbox: false,
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
const wc = mainWindow.webContents;
|
|
156
|
-
if (!electron_1.app.isPackaged)
|
|
157
|
-
wc.openDevTools({ mode: 'detach' });
|
|
158
|
-
wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
|
|
159
|
-
wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
|
|
160
|
-
wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
|
|
161
|
-
wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
|
|
162
|
-
wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
|
|
163
|
-
mainWindow.on('closed', () => {
|
|
164
|
-
mainWindow = null;
|
|
165
|
-
});
|
|
166
|
-
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
167
|
-
if (url.startsWith(startUrl))
|
|
168
|
-
return { action: 'allow' };
|
|
169
|
-
void electron_1.shell.openExternal(url);
|
|
170
|
-
return { action: 'deny' };
|
|
171
|
-
});
|
|
172
|
-
void mainWindow.loadURL(startUrl).catch((err) => {
|
|
173
|
-
console.error('[swarmclaw] loadURL rejected:', err);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
async function showServerCrashDialog(code, signal) {
|
|
177
|
-
const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
|
|
178
|
-
const quitButtonId = buttons.length - 1;
|
|
179
|
-
const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
|
|
180
|
-
const res = await electron_1.dialog.showMessageBox({
|
|
181
|
-
type: 'error',
|
|
182
|
-
buttons,
|
|
183
|
-
defaultId: quitButtonId,
|
|
184
|
-
cancelId: quitButtonId,
|
|
185
|
-
title: 'SwarmClaw stopped',
|
|
186
|
-
message: 'The SwarmClaw server exited unexpectedly.',
|
|
187
|
-
detail,
|
|
188
|
-
});
|
|
189
|
-
if (serverLogFile && res.response === 0)
|
|
190
|
-
electron_1.shell.showItemInFolder(serverLogFile);
|
|
191
|
-
electron_1.app.exit(1);
|
|
192
|
-
}
|
|
193
|
-
async function showStartupFailureDialog(err, paths) {
|
|
194
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
-
const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
|
|
196
|
-
const detail = buildLogDetail(base);
|
|
197
|
-
const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
|
|
198
|
-
const quitButtonId = buttons.length - 1;
|
|
199
|
-
const res = await electron_1.dialog.showMessageBox({
|
|
200
|
-
type: 'error',
|
|
201
|
-
buttons,
|
|
202
|
-
defaultId: quitButtonId,
|
|
203
|
-
cancelId: quitButtonId,
|
|
204
|
-
title: 'SwarmClaw failed to start',
|
|
205
|
-
message: 'The embedded server did not start.',
|
|
206
|
-
detail,
|
|
207
|
-
});
|
|
208
|
-
if (serverLogFile && res.response === 0)
|
|
209
|
-
electron_1.shell.showItemInFolder(serverLogFile);
|
|
210
|
-
}
|
|
211
|
-
function buildLogDetail(base) {
|
|
212
|
-
if (!serverLogFile)
|
|
213
|
-
return base;
|
|
214
|
-
const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
|
|
215
|
-
if (!tail)
|
|
216
|
-
return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
|
|
217
|
-
return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
|
|
218
|
-
}
|