bazilion 0.0.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/.understand-anything/.understandignore +25 -0
- package/.understand-anything/fingerprints.json +14267 -0
- package/.understand-anything/knowledge-graph.json +18128 -0
- package/.understand-anything/meta.json +6 -0
- package/CLAUDE.md +164 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/apps/cli/package.json +21 -0
- package/apps/cli/src/auth-file.ts +18 -0
- package/apps/cli/src/client.ts +37 -0
- package/apps/cli/src/columnize.ts +32 -0
- package/apps/cli/src/commands/agent.ts +574 -0
- package/apps/cli/src/commands/auth.ts +110 -0
- package/apps/cli/src/commands/backup.ts +135 -0
- package/apps/cli/src/commands/completion.ts +155 -0
- package/apps/cli/src/commands/config.ts +95 -0
- package/apps/cli/src/commands/doctor.ts +131 -0
- package/apps/cli/src/commands/group.ts +132 -0
- package/apps/cli/src/commands/inbox.ts +82 -0
- package/apps/cli/src/commands/login.ts +73 -0
- package/apps/cli/src/commands/memory.ts +106 -0
- package/apps/cli/src/commands/profile.ts +259 -0
- package/apps/cli/src/commands/provider.ts +170 -0
- package/apps/cli/src/commands/send.ts +24 -0
- package/apps/cli/src/commands/serve.ts +89 -0
- package/apps/cli/src/commands/skill.ts +120 -0
- package/apps/cli/src/commands/token.ts +148 -0
- package/apps/cli/src/commands/trigger.ts +129 -0
- package/apps/cli/src/commands/uninstall.ts +156 -0
- package/apps/cli/src/index.ts +196 -0
- package/apps/cli/src/paths.ts +12 -0
- package/apps/cli/test/agent.test.ts +213 -0
- package/apps/cli/test/backup.test.ts +95 -0
- package/apps/cli/test/chat.test.ts +539 -0
- package/apps/cli/test/columnize.test.ts +29 -0
- package/apps/cli/test/completion.test.ts +45 -0
- package/apps/cli/test/config-page.test.ts +151 -0
- package/apps/cli/test/group.test.ts +74 -0
- package/apps/cli/test/helpers.ts +70 -0
- package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
- package/apps/cli/test/inbox.test.ts +147 -0
- package/apps/cli/test/memory.test.ts +69 -0
- package/apps/cli/test/profile.test.ts +110 -0
- package/apps/cli/test/send.test.ts +30 -0
- package/apps/cli/test/server-fixture.ts +212 -0
- package/apps/cli/test/session-head.test.ts +45 -0
- package/apps/cli/test/skill.test.ts +128 -0
- package/apps/cli/test/token.test.ts +109 -0
- package/apps/cli/test/trigger.test.ts +245 -0
- package/apps/cli/tsconfig.json +4 -0
- package/apps/daemon/package.json +31 -0
- package/apps/daemon/src/app.ts +41 -0
- package/apps/daemon/src/core/agent/archive.ts +8 -0
- package/apps/daemon/src/core/agent/delete.ts +30 -0
- package/apps/daemon/src/core/agent/resolve.ts +31 -0
- package/apps/daemon/src/core/agent/spawn.ts +95 -0
- package/apps/daemon/src/core/agent/unarchive.ts +11 -0
- package/apps/daemon/src/core/availableModels.ts +58 -0
- package/apps/daemon/src/core/db/client.ts +113 -0
- package/apps/daemon/src/core/db/migrate.ts +41 -0
- package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
- package/apps/daemon/src/core/group/delete.ts +21 -0
- package/apps/daemon/src/core/group/register.ts +68 -0
- package/apps/daemon/src/core/index.ts +71 -0
- package/apps/daemon/src/core/paths.ts +56 -0
- package/apps/daemon/src/core/profile/create.ts +78 -0
- package/apps/daemon/src/core/profile/delete.ts +26 -0
- package/apps/daemon/src/core/profile/identity.ts +70 -0
- package/apps/daemon/src/core/profile/load.ts +45 -0
- package/apps/daemon/src/core/profile/seed.ts +74 -0
- package/apps/daemon/src/core/profile/templates.ts +60 -0
- package/apps/daemon/src/core/profile/update.ts +57 -0
- package/apps/daemon/src/core/profile/validate.ts +9 -0
- package/apps/daemon/src/core/repos/agents.ts +202 -0
- package/apps/daemon/src/core/repos/config.ts +78 -0
- package/apps/daemon/src/core/repos/groups.ts +55 -0
- package/apps/daemon/src/core/repos/messages.ts +127 -0
- package/apps/daemon/src/core/repos/profiles.ts +83 -0
- package/apps/daemon/src/core/repos/providerModels.ts +58 -0
- package/apps/daemon/src/core/repos/providerState.ts +37 -0
- package/apps/daemon/src/core/repos/secrets.ts +145 -0
- package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
- package/apps/daemon/src/core/repos/triggers.ts +101 -0
- package/apps/daemon/src/core/repos/webTokens.ts +87 -0
- package/apps/daemon/src/core/secrets.ts +65 -0
- package/apps/daemon/src/core/services.ts +264 -0
- package/apps/daemon/src/core/skills/discover.ts +28 -0
- package/apps/daemon/src/core/skills/import.ts +136 -0
- package/apps/daemon/src/core/skills/parse.ts +52 -0
- package/apps/daemon/src/core/skills/resolve.ts +50 -0
- package/apps/daemon/src/index.ts +45 -0
- package/apps/daemon/src/lib/agent-cancel.ts +48 -0
- package/apps/daemon/src/lib/agent-id.ts +13 -0
- package/apps/daemon/src/lib/agent-turn.ts +56 -0
- package/apps/daemon/src/lib/api-key.ts +56 -0
- package/apps/daemon/src/lib/auth.ts +41 -0
- package/apps/daemon/src/lib/cron.ts +93 -0
- package/apps/daemon/src/lib/ctx.ts +80 -0
- package/apps/daemon/src/lib/messaging-host.ts +34 -0
- package/apps/daemon/src/lib/middleware-auth.ts +52 -0
- package/apps/daemon/src/lib/scheduler.ts +294 -0
- package/apps/daemon/src/routes/agents.ts +772 -0
- package/apps/daemon/src/routes/auth-login.ts +193 -0
- package/apps/daemon/src/routes/config.ts +267 -0
- package/apps/daemon/src/routes/groups.ts +133 -0
- package/apps/daemon/src/routes/messages.ts +29 -0
- package/apps/daemon/src/routes/misc.ts +239 -0
- package/apps/daemon/src/routes/profiles.ts +197 -0
- package/apps/daemon/src/routes/skills.ts +123 -0
- package/apps/daemon/src/routes/triggers.ts +29 -0
- package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
- package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
- package/apps/daemon/src/runtime/index.ts +77 -0
- package/apps/daemon/src/runtime/memory/files.ts +103 -0
- package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
- package/apps/daemon/src/runtime/memory/types.ts +16 -0
- package/apps/daemon/src/runtime/pi/events.ts +173 -0
- package/apps/daemon/src/runtime/pi/session.ts +536 -0
- package/apps/daemon/src/runtime/pi/tools.ts +85 -0
- package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
- package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
- package/apps/daemon/src/runtime/providers/registry.ts +374 -0
- package/apps/daemon/src/runtime/providers/retry.ts +176 -0
- package/apps/daemon/src/runtime/providers/types.ts +33 -0
- package/apps/daemon/src/runtime/session/prompt.ts +83 -0
- package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
- package/apps/daemon/src/runtime/tools/home.ts +114 -0
- package/apps/daemon/src/runtime/tools/memory.ts +81 -0
- package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
- package/apps/daemon/src/runtime/tools/registry.ts +29 -0
- package/apps/daemon/src/runtime/tools/types.ts +13 -0
- package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
- package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
- package/apps/daemon/src/runtime/tools/web.ts +273 -0
- package/apps/daemon/src/runtime/worker/entry.ts +221 -0
- package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
- package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
- package/apps/daemon/test/core/agents.test.ts +319 -0
- package/apps/daemon/test/core/available-models.test.ts +63 -0
- package/apps/daemon/test/core/config.test.ts +99 -0
- package/apps/daemon/test/core/groups.test.ts +63 -0
- package/apps/daemon/test/core/helpers.ts +50 -0
- package/apps/daemon/test/core/identity.test.ts +85 -0
- package/apps/daemon/test/core/migrations.test.ts +83 -0
- package/apps/daemon/test/core/profiles.test.ts +182 -0
- package/apps/daemon/test/core/provider-models.test.ts +57 -0
- package/apps/daemon/test/core/provider-state.test.ts +36 -0
- package/apps/daemon/test/core/skill-meta.test.ts +45 -0
- package/apps/daemon/test/core/skills.test.ts +271 -0
- package/apps/daemon/test/core/triggers.test.ts +182 -0
- package/apps/daemon/test/core/web-tokens.test.ts +79 -0
- package/apps/daemon/test/cron.test.ts +90 -0
- package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
- package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
- package/apps/daemon/test/runtime/memory.test.ts +78 -0
- package/apps/daemon/test/runtime/messaging.test.ts +213 -0
- package/apps/daemon/test/runtime/mock-server.ts +65 -0
- package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
- package/apps/daemon/test/runtime/providers.test.ts +306 -0
- package/apps/daemon/test/runtime/retry.test.ts +191 -0
- package/apps/daemon/test/runtime/session-head.test.ts +90 -0
- package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
- package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
- package/apps/daemon/tsconfig.json +4 -0
- package/apps/mobile/README.md +60 -0
- package/apps/mobile/app/_layout.tsx +58 -0
- package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
- package/apps/mobile/app/agents/[id]/index.tsx +166 -0
- package/apps/mobile/app/agents/index.tsx +212 -0
- package/apps/mobile/app/index.tsx +21 -0
- package/apps/mobile/app/pair.tsx +226 -0
- package/apps/mobile/app/settings.tsx +419 -0
- package/apps/mobile/app.json +49 -0
- package/apps/mobile/assets/adaptive-icon.png +0 -0
- package/apps/mobile/assets/favicon.png +0 -0
- package/apps/mobile/assets/icon.png +0 -0
- package/apps/mobile/assets/splash-icon.png +0 -0
- package/apps/mobile/babel.config.js +6 -0
- package/apps/mobile/metro.config.js +28 -0
- package/apps/mobile/package.json +44 -0
- package/apps/mobile/src/auth.ts +66 -0
- package/apps/mobile/src/pair-url.ts +48 -0
- package/apps/mobile/src/theme-context.tsx +88 -0
- package/apps/mobile/src/theme.ts +135 -0
- package/apps/mobile/test/pair-url.test.ts +46 -0
- package/apps/mobile/tsconfig.json +23 -0
- package/apps/web/components.json +25 -0
- package/apps/web/package.json +39 -0
- package/apps/web/public/baziu.svg +8 -0
- package/apps/web/src/components/AgentTabs.tsx +45 -0
- package/apps/web/src/components/BaziuLogo.tsx +21 -0
- package/apps/web/src/components/ChatPane.tsx +1033 -0
- package/apps/web/src/components/ConfigTabs.tsx +29 -0
- package/apps/web/src/components/CopyButton.tsx +68 -0
- package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
- package/apps/web/src/components/FieldRow.tsx +94 -0
- package/apps/web/src/components/Footer.tsx +10 -0
- package/apps/web/src/components/PawIcon.tsx +15 -0
- package/apps/web/src/components/Sidebar.tsx +287 -0
- package/apps/web/src/components/SpawnDialog.tsx +129 -0
- package/apps/web/src/components/ThemeToggle.tsx +75 -0
- package/apps/web/src/components/TopNav.tsx +34 -0
- package/apps/web/src/components/ui/button.tsx +67 -0
- package/apps/web/src/components/ui/card.tsx +103 -0
- package/apps/web/src/components/ui/checkbox.tsx +31 -0
- package/apps/web/src/components/ui/dialog.tsx +168 -0
- package/apps/web/src/components/ui/input.tsx +19 -0
- package/apps/web/src/components/ui/label.tsx +22 -0
- package/apps/web/src/components/ui/radio-group.tsx +44 -0
- package/apps/web/src/components/ui/select.tsx +192 -0
- package/apps/web/src/components/ui/separator.tsx +26 -0
- package/apps/web/src/components/ui/table.tsx +116 -0
- package/apps/web/src/components/ui/tabs.tsx +88 -0
- package/apps/web/src/components/ui/textarea.tsx +18 -0
- package/apps/web/src/lib/auth.ts +50 -0
- package/apps/web/src/lib/daemon-client.ts +34 -0
- package/apps/web/src/lib/md.ts +45 -0
- package/apps/web/src/lib/utils.ts +6 -0
- package/apps/web/src/lib/wire-constants.ts +27 -0
- package/apps/web/src/routeTree.gen.ts +408 -0
- package/apps/web/src/router.tsx +20 -0
- package/apps/web/src/routes/__root.tsx +123 -0
- package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
- package/apps/web/src/routes/agents/$id/index.tsx +527 -0
- package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
- package/apps/web/src/routes/agents/index.tsx +265 -0
- package/apps/web/src/routes/api/$.ts +88 -0
- package/apps/web/src/routes/config/index.tsx +315 -0
- package/apps/web/src/routes/config/services.tsx +49 -0
- package/apps/web/src/routes/config/tokens.tsx +192 -0
- package/apps/web/src/routes/groups/$id/index.tsx +153 -0
- package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
- package/apps/web/src/routes/groups/index.tsx +191 -0
- package/apps/web/src/routes/index.tsx +133 -0
- package/apps/web/src/routes/login.tsx +63 -0
- package/apps/web/src/routes/profiles/$id.tsx +549 -0
- package/apps/web/src/routes/profiles/index.tsx +458 -0
- package/apps/web/src/routes/skills/index.tsx +297 -0
- package/apps/web/src/routes/welcome.tsx +61 -0
- package/apps/web/src/styles.css +449 -0
- package/apps/web/tsconfig.json +25 -0
- package/apps/web/vite.config.ts +25 -0
- package/biome.json +23 -0
- package/docs/agent-engine.md +219 -0
- package/docs/architecture.md +627 -0
- package/docs/backlog/README.md +42 -0
- package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
- package/docs/openclaw-reference.md +210 -0
- package/package.json +38 -0
- package/packages/api-types/package.json +11 -0
- package/packages/api-types/src/entities.ts +146 -0
- package/packages/api-types/src/events.ts +55 -0
- package/packages/api-types/src/index.ts +488 -0
- package/packages/api-types/src/memory.ts +15 -0
- package/packages/client/package.json +13 -0
- package/packages/client/src/index.ts +117 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Sub-nav for the /config pages tree.
|
|
2
|
+
|
|
3
|
+
type Tab = 'providers' | 'services' | 'tokens'
|
|
4
|
+
|
|
5
|
+
const TABS: { key: Tab; href: string; label: string }[] = [
|
|
6
|
+
{ key: 'providers', href: '/config', label: 'providers' },
|
|
7
|
+
{ key: 'services', href: '/config/services', label: 'services' },
|
|
8
|
+
{ key: 'tokens', href: '/config/tokens', label: 'tokens' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export function ConfigTabs({ active }: { active: Tab }) {
|
|
12
|
+
return (
|
|
13
|
+
<nav className="flex gap-1 border-b mb-6">
|
|
14
|
+
{TABS.map((t) => (
|
|
15
|
+
<a
|
|
16
|
+
key={t.key}
|
|
17
|
+
href={t.href}
|
|
18
|
+
className={`px-3 py-2 text-sm border-b-2 -mb-px ${
|
|
19
|
+
t.key === active
|
|
20
|
+
? 'border-primary text-primary font-semibold'
|
|
21
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
22
|
+
}`}
|
|
23
|
+
>
|
|
24
|
+
{t.label}
|
|
25
|
+
</a>
|
|
26
|
+
))}
|
|
27
|
+
</nav>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Tiny clipboard button used by the agent detail header (copies the full UUID
|
|
2
|
+
// for inter-agent messaging). Async clipboard API where available, transient
|
|
3
|
+
// <textarea> + execCommand fallback for http://127… contexts where Chromium
|
|
4
|
+
// blocks the async API. Flashes a green ✓ for ~1s on success.
|
|
5
|
+
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
value: string
|
|
10
|
+
className?: string
|
|
11
|
+
ariaLabel?: string
|
|
12
|
+
title?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CopyButton({
|
|
16
|
+
value,
|
|
17
|
+
className,
|
|
18
|
+
ariaLabel = 'Copy to clipboard',
|
|
19
|
+
title = 'Copy to clipboard',
|
|
20
|
+
}: Props) {
|
|
21
|
+
const [copied, setCopied] = useState(false)
|
|
22
|
+
|
|
23
|
+
async function copy() {
|
|
24
|
+
if (!value) return
|
|
25
|
+
let ok = false
|
|
26
|
+
try {
|
|
27
|
+
if (navigator.clipboard?.writeText) {
|
|
28
|
+
await navigator.clipboard.writeText(value)
|
|
29
|
+
ok = true
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// fall through
|
|
33
|
+
}
|
|
34
|
+
if (!ok) {
|
|
35
|
+
const ta = document.createElement('textarea')
|
|
36
|
+
ta.value = value
|
|
37
|
+
ta.setAttribute('readonly', '')
|
|
38
|
+
ta.style.position = 'fixed'
|
|
39
|
+
ta.style.opacity = '0'
|
|
40
|
+
document.body.appendChild(ta)
|
|
41
|
+
ta.select()
|
|
42
|
+
try {
|
|
43
|
+
ok = document.execCommand('copy')
|
|
44
|
+
} catch {}
|
|
45
|
+
ta.remove()
|
|
46
|
+
}
|
|
47
|
+
if (ok) {
|
|
48
|
+
setCopied(true)
|
|
49
|
+
setTimeout(() => setCopied(false), 1000)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const base =
|
|
54
|
+
'inline-flex h-[1.3rem] w-[1.6rem] items-center justify-center rounded-sm border border-transparent text-mocha-light transition-colors hover:border-frost hover:bg-snow hover:text-mocha'
|
|
55
|
+
const success =
|
|
56
|
+
'border-[#bcd9bc] bg-[#eef7ee] text-[#3b7a3b] hover:border-[#bcd9bc] hover:bg-[#eef7ee] hover:text-[#3b7a3b]'
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={copy}
|
|
61
|
+
title={title}
|
|
62
|
+
aria-label={ariaLabel}
|
|
63
|
+
className={`${base} ${copied ? success : ''} ${className ?? ''}`}
|
|
64
|
+
>
|
|
65
|
+
{copied ? '✓' : '⧉'}
|
|
66
|
+
</button>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Quick-create a group from the sidebar's spawn dropdown. POSTs to
|
|
2
|
+
// /api/groups with the current shape: { id, name?, link? }. The daemon
|
|
3
|
+
// puts the slot under ~/.bazilion/groups/<slug>/ — a fresh directory by
|
|
4
|
+
// default, or a symlink to `link` if provided.
|
|
5
|
+
|
|
6
|
+
import { useRouter } from '@tanstack/react-router'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CreateGroupDialog({ onClose }: Props) {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const [id, setId] = useState('')
|
|
16
|
+
const [name, setName] = useState('')
|
|
17
|
+
const [link, setLink] = useState('')
|
|
18
|
+
const [busy, setBusy] = useState(false)
|
|
19
|
+
const [err, setErr] = useState<string | null>(null)
|
|
20
|
+
|
|
21
|
+
async function submit(e: React.FormEvent) {
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
setErr(null)
|
|
24
|
+
if (!id.trim()) {
|
|
25
|
+
setErr('id is required')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
setBusy(true)
|
|
29
|
+
try {
|
|
30
|
+
const body: Record<string, unknown> = { id: id.trim() }
|
|
31
|
+
if (name.trim()) body.name = name.trim()
|
|
32
|
+
if (link.trim()) body.link = link.trim()
|
|
33
|
+
const res = await fetch('/api/groups', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'content-type': 'application/json' },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
})
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const e = (await res.json().catch(() => ({}))) as { error?: string }
|
|
40
|
+
throw new Error(e.error ?? `${res.status} ${res.statusText}`)
|
|
41
|
+
}
|
|
42
|
+
onClose()
|
|
43
|
+
await router.invalidate()
|
|
44
|
+
} catch (e) {
|
|
45
|
+
setErr((e as Error).message)
|
|
46
|
+
setBusy(false)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: backdrop click-to-close is augmentative
|
|
52
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: ditto
|
|
53
|
+
<div
|
|
54
|
+
onClick={onClose}
|
|
55
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-[1px]"
|
|
56
|
+
>
|
|
57
|
+
<form
|
|
58
|
+
onSubmit={submit}
|
|
59
|
+
onClick={(e) => e.stopPropagation()}
|
|
60
|
+
className="w-full max-w-md rounded-2xl border bg-card p-6 shadow-lg"
|
|
61
|
+
>
|
|
62
|
+
<h3 className="font-serif text-xl text-foreground mb-1">Create a new group</h3>
|
|
63
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
64
|
+
A group is a collaboration context — one filesystem root, one USER.md, one roster. The
|
|
65
|
+
slot lives at <code className="font-mono">~/.bazilion/groups/<slug>/</code>.
|
|
66
|
+
</p>
|
|
67
|
+
<div className="grid grid-cols-2 gap-3 mb-3">
|
|
68
|
+
<label className="block text-sm">
|
|
69
|
+
ID (slug)
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
value={id}
|
|
73
|
+
onChange={(e) => setId(e.target.value)}
|
|
74
|
+
required
|
|
75
|
+
pattern="[a-z0-9][a-z0-9_-]*"
|
|
76
|
+
placeholder="myproject"
|
|
77
|
+
// biome-ignore lint/a11y/noAutofocus: dialog convention
|
|
78
|
+
autoFocus
|
|
79
|
+
className="mt-1 block w-full rounded-md border bg-background px-3 py-2 outline-none focus:ring-2 focus:ring-ring/30"
|
|
80
|
+
/>
|
|
81
|
+
</label>
|
|
82
|
+
<label className="block text-sm">
|
|
83
|
+
Name <span className="text-muted-foreground font-normal">(optional)</span>
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
value={name}
|
|
87
|
+
onChange={(e) => setName(e.target.value)}
|
|
88
|
+
className="mt-1 block w-full rounded-md border bg-background px-3 py-2 outline-none focus:ring-2 focus:ring-ring/30"
|
|
89
|
+
/>
|
|
90
|
+
</label>
|
|
91
|
+
</div>
|
|
92
|
+
<label className="block text-sm mb-3">
|
|
93
|
+
Link target{' '}
|
|
94
|
+
<span className="text-muted-foreground font-normal">(optional, absolute path)</span>
|
|
95
|
+
<input
|
|
96
|
+
type="text"
|
|
97
|
+
value={link}
|
|
98
|
+
onChange={(e) => setLink(e.target.value)}
|
|
99
|
+
placeholder="/home/you/projects/myrepo"
|
|
100
|
+
className="mt-1 block w-full rounded-md border bg-background px-3 py-2 outline-none focus:ring-2 focus:ring-ring/30"
|
|
101
|
+
/>
|
|
102
|
+
<span className="mt-1 block text-xs text-muted-foreground">
|
|
103
|
+
Leave blank to create a fresh directory. Supply an absolute path to materialize the
|
|
104
|
+
slot as a symlink to your existing project tree.
|
|
105
|
+
</span>
|
|
106
|
+
</label>
|
|
107
|
+
{err && <p className="mt-2 text-sm text-rose-700">{err}</p>}
|
|
108
|
+
<div className="mt-5 flex justify-end gap-2">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={onClose}
|
|
112
|
+
className="rounded-md border px-3 py-2 text-sm text-foreground hover:bg-accent"
|
|
113
|
+
>
|
|
114
|
+
Cancel
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
type="submit"
|
|
118
|
+
disabled={busy}
|
|
119
|
+
className="rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50"
|
|
120
|
+
>
|
|
121
|
+
{busy ? 'Creating…' : 'Create'}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</form>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Single config field with inline save. Shared between the providers and
|
|
2
|
+
// services pages — both use the same /api/config/fields/:envVar PUT path.
|
|
3
|
+
|
|
4
|
+
import type { ServiceFieldState } from '@bazilion/api-types'
|
|
5
|
+
import { useRouter } from '@tanstack/react-router'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
|
|
8
|
+
export function FieldRow({ field }: { field: ServiceFieldState }) {
|
|
9
|
+
const router = useRouter()
|
|
10
|
+
const [value, setValue] = useState(field.kind === 'config' ? (field.value ?? '') : '')
|
|
11
|
+
const [busy, setBusy] = useState(false)
|
|
12
|
+
const [err, setErr] = useState<string | null>(null)
|
|
13
|
+
const [savedAt, setSavedAt] = useState<number | null>(null)
|
|
14
|
+
|
|
15
|
+
async function save(e: React.FormEvent) {
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
setBusy(true)
|
|
18
|
+
setErr(null)
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`/api/config/fields/${encodeURIComponent(field.envVar)}`, {
|
|
21
|
+
method: 'PUT',
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ value }),
|
|
24
|
+
})
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const j = (await res.json().catch(() => ({}))) as { error?: string }
|
|
27
|
+
throw new Error(j.error ?? `${res.status} ${res.statusText}`)
|
|
28
|
+
}
|
|
29
|
+
setSavedAt(Date.now())
|
|
30
|
+
// Secret fields don't echo the value back; clear local state so the
|
|
31
|
+
// input is empty (and the "set" pill updates via invalidation).
|
|
32
|
+
if (field.kind === 'secret') setValue('')
|
|
33
|
+
await router.invalidate()
|
|
34
|
+
} catch (e) {
|
|
35
|
+
setErr((e as Error).message)
|
|
36
|
+
} finally {
|
|
37
|
+
setBusy(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<form
|
|
43
|
+
onSubmit={save}
|
|
44
|
+
className="grid grid-cols-[12rem_minmax(14rem,1fr)_auto_auto] gap-2 items-center py-2 border-b last:border-0"
|
|
45
|
+
>
|
|
46
|
+
<div className="min-w-0">
|
|
47
|
+
<div className="text-sm">{field.label}</div>
|
|
48
|
+
<div className="font-mono text-xs text-muted-foreground">{field.envVar}</div>
|
|
49
|
+
{field.description && (
|
|
50
|
+
<div className="text-xs text-muted-foreground mt-0.5">{field.description}</div>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
{field.kind === 'config' ? (
|
|
54
|
+
<input
|
|
55
|
+
type="text"
|
|
56
|
+
value={value}
|
|
57
|
+
onChange={(e) => setValue(e.target.value)}
|
|
58
|
+
placeholder={field.placeholder ?? ''}
|
|
59
|
+
className="rounded-md border bg-background px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring/30"
|
|
60
|
+
/>
|
|
61
|
+
) : (
|
|
62
|
+
<input
|
|
63
|
+
type="password"
|
|
64
|
+
value={value}
|
|
65
|
+
onChange={(e) => setValue(e.target.value)}
|
|
66
|
+
placeholder={field.set ? '(replace — blank clears)' : (field.placeholder ?? 'paste key…')}
|
|
67
|
+
autoComplete="off"
|
|
68
|
+
className="rounded-md border bg-background px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring/30"
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
<span className="text-xs">
|
|
72
|
+
{field.kind === 'secret' && field.set && (
|
|
73
|
+
<span
|
|
74
|
+
className="rounded-full bg-emerald-100 text-emerald-800 px-1.5 py-0.5"
|
|
75
|
+
title={`preview: ${field.preview ?? ''}`}
|
|
76
|
+
>
|
|
77
|
+
set
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{savedAt && Date.now() - savedAt < 2000 && (
|
|
81
|
+
<span className="text-emerald-700 ml-2">saved ✓</span>
|
|
82
|
+
)}
|
|
83
|
+
</span>
|
|
84
|
+
<button
|
|
85
|
+
type="submit"
|
|
86
|
+
disabled={busy}
|
|
87
|
+
className="rounded-md border bg-background px-2 py-1.5 text-xs hover:bg-accent disabled:opacity-50"
|
|
88
|
+
>
|
|
89
|
+
save
|
|
90
|
+
</button>
|
|
91
|
+
{err && <p className="col-span-4 text-xs text-rose-700">{err}</p>}
|
|
92
|
+
</form>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PawIcon } from './PawIcon'
|
|
2
|
+
|
|
3
|
+
export function Footer() {
|
|
4
|
+
return (
|
|
5
|
+
<footer className="mt-16 border-t border-frost py-6 text-center text-[0.82em] text-fawn">
|
|
6
|
+
dedicated to Baziu
|
|
7
|
+
<PawIcon className="ml-1 inline-block h-3 w-3 align-[-1px] opacity-40" />
|
|
8
|
+
</footer>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface PawIconProps {
|
|
2
|
+
className?: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function PawIcon({ className }: PawIconProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true">
|
|
8
|
+
<ellipse cx="7" cy="5" rx="2.2" ry="2.8" />
|
|
9
|
+
<ellipse cx="17" cy="5" rx="2.2" ry="2.8" />
|
|
10
|
+
<ellipse cx="3.5" cy="11" rx="2" ry="2.5" />
|
|
11
|
+
<ellipse cx="20.5" cy="11" rx="2" ry="2.5" />
|
|
12
|
+
<path d="M12 22c-4.5 0-7.5-3-7.5-6 0-3 2.5-5.5 4.5-7a4 4 0 0 1 6 0c2 1.5 4.5 4 4.5 7 0 3-3 6-7.5 6z" />
|
|
13
|
+
</svg>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Left sidebar: collapsible groups + agent rows + spawn dropdown. Each agent
|
|
2
|
+
// row reveals rename (✎) and archive (×) buttons on hover.
|
|
3
|
+
|
|
4
|
+
import type { Agent, Group, Profile } from '@bazilion/api-types'
|
|
5
|
+
import { Link, useRouter } from '@tanstack/react-router'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { DEFAULT_GROUP_ID, DEFAULT_PROFILE_ID } from '../lib/wire-constants'
|
|
8
|
+
import { CreateGroupDialog } from './CreateGroupDialog'
|
|
9
|
+
import { SpawnDialog } from './SpawnDialog'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
agents: Agent[]
|
|
13
|
+
groups: Group[]
|
|
14
|
+
profiles: Profile[]
|
|
15
|
+
selectedAgentId: string | null
|
|
16
|
+
/** Per-group open/closed map seeded by SSR from the cookie. */
|
|
17
|
+
initialOpenGroups?: Record<string, boolean>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Cookie name shared with the SSR loader in `routes/index.tsx`. Stored as
|
|
21
|
+
// URL-encoded JSON of `Record<string, boolean>` — keys are group IDs, values
|
|
22
|
+
// are the user's explicit open/closed preference. Used over localStorage so
|
|
23
|
+
// the SSR render lands with the correct state and the user never sees a flash
|
|
24
|
+
// of default state before hydration corrects it.
|
|
25
|
+
export const SIDEBAR_OPEN_GROUPS_COOKIE = 'bz_sidebar_open_groups'
|
|
26
|
+
|
|
27
|
+
function writeOpenGroupsCookie(map: Record<string, boolean>): void {
|
|
28
|
+
if (typeof document === 'undefined') return
|
|
29
|
+
const value = encodeURIComponent(JSON.stringify(map))
|
|
30
|
+
// 1-year retention, Path=/ so every route sees it, Lax for default safety.
|
|
31
|
+
document.cookie = `${SIDEBAR_OPEN_GROUPS_COOKIE}=${value}; Path=/; Max-Age=31536000; SameSite=Lax`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Sidebar({
|
|
35
|
+
agents,
|
|
36
|
+
groups,
|
|
37
|
+
profiles,
|
|
38
|
+
selectedAgentId,
|
|
39
|
+
initialOpenGroups,
|
|
40
|
+
}: Props) {
|
|
41
|
+
const router = useRouter()
|
|
42
|
+
const [spawnFor, setSpawnFor] = useState<{ profileId: string; groupHint?: string } | null>(null)
|
|
43
|
+
const [createGroupOpen, setCreateGroupOpen] = useState(false)
|
|
44
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
45
|
+
// Seeded by SSR from the cookie so the first paint matches the user's
|
|
46
|
+
// saved preferences — no flash of default state.
|
|
47
|
+
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>(
|
|
48
|
+
() => initialOpenGroups ?? {},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async function rename(a: Agent) {
|
|
52
|
+
const next = window.prompt(`rename "${a.name}" to:`, a.name)?.trim()
|
|
53
|
+
if (!next || next === a.name) return
|
|
54
|
+
const res = await fetch(`/api/agents/${a.id}`, {
|
|
55
|
+
method: 'PATCH',
|
|
56
|
+
headers: { 'content-type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ name: next }),
|
|
58
|
+
})
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = (await res.json().catch(() => null)) as { error?: string } | null
|
|
61
|
+
alert(body?.error ?? res.statusText)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
await router.invalidate()
|
|
65
|
+
}
|
|
66
|
+
async function archive(a: Agent) {
|
|
67
|
+
if (!confirm(`archive "${a.name}"? (reversible — find under "show archived")`)) return
|
|
68
|
+
const res = await fetch(`/api/agents/${a.id}/archive`, { method: 'POST' })
|
|
69
|
+
if (!res.ok && res.status !== 204) {
|
|
70
|
+
alert(res.statusText)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
await router.invalidate()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Float the seeded `default` profile + group to the top so new users land
|
|
77
|
+
// on the one-click spawn path.
|
|
78
|
+
const sortedProfiles = [...profiles].sort((a, b) => {
|
|
79
|
+
if (a.id === DEFAULT_PROFILE_ID) return -1
|
|
80
|
+
if (b.id === DEFAULT_PROFILE_ID) return 1
|
|
81
|
+
return 0
|
|
82
|
+
})
|
|
83
|
+
const sortedGroups = [...groups].sort((a, b) => {
|
|
84
|
+
if (a.id === DEFAULT_GROUP_ID) return -1
|
|
85
|
+
if (b.id === DEFAULT_GROUP_ID) return 1
|
|
86
|
+
return 0
|
|
87
|
+
})
|
|
88
|
+
const agentsByGroup = new Map<string, Agent[]>()
|
|
89
|
+
for (const a of agents) {
|
|
90
|
+
const list = agentsByGroup.get(a.groupId) ?? []
|
|
91
|
+
list.push(a)
|
|
92
|
+
agentsByGroup.set(a.groupId, list)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const selectedGroupId = agents.find((a) => a.id === selectedAgentId)?.groupId ?? null
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<aside className="flex h-full flex-col rounded-lg border bg-card overflow-hidden">
|
|
99
|
+
<header className="flex items-center justify-between border-b px-3 py-2.5 bg-muted/30">
|
|
100
|
+
<span className="font-serif text-base text-foreground">agents</span>
|
|
101
|
+
<div className="relative">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
105
|
+
className="rounded-md border px-2 py-1 text-xs text-muted-foreground hover:text-foreground hover:border-foreground/30"
|
|
106
|
+
>
|
|
107
|
+
+ new ▾
|
|
108
|
+
</button>
|
|
109
|
+
{menuOpen && (
|
|
110
|
+
<div
|
|
111
|
+
role="menu"
|
|
112
|
+
className="absolute right-0 top-[calc(100%+0.3rem)] z-20 min-w-[12rem] rounded-md border bg-popover p-1 shadow-md"
|
|
113
|
+
onMouseLeave={() => setMenuOpen(false)}
|
|
114
|
+
>
|
|
115
|
+
{profiles.length === 0 ? (
|
|
116
|
+
<div className="px-3 py-2 text-sm text-muted-foreground">
|
|
117
|
+
No profiles yet.
|
|
118
|
+
<br />
|
|
119
|
+
<a href="/profiles" className="text-primary underline">
|
|
120
|
+
Create one
|
|
121
|
+
</a>{' '}
|
|
122
|
+
to spawn agents.
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
<>
|
|
126
|
+
<div className="px-3 pt-1 pb-0.5 text-[0.7em] uppercase tracking-wide text-muted-foreground">
|
|
127
|
+
spawn from profile
|
|
128
|
+
</div>
|
|
129
|
+
{sortedProfiles.map((p) => (
|
|
130
|
+
<button
|
|
131
|
+
key={p.id}
|
|
132
|
+
type="button"
|
|
133
|
+
role="menuitem"
|
|
134
|
+
onClick={() => {
|
|
135
|
+
setMenuOpen(false)
|
|
136
|
+
setSpawnFor({ profileId: p.id })
|
|
137
|
+
}}
|
|
138
|
+
className="flex w-full items-center justify-between rounded-sm px-3 py-1.5 text-sm hover:bg-accent"
|
|
139
|
+
>
|
|
140
|
+
<span>{p.name || p.id}</span>
|
|
141
|
+
{p.id === DEFAULT_PROFILE_ID && (
|
|
142
|
+
<span className="rounded bg-muted px-1 text-[0.7em] text-muted-foreground">
|
|
143
|
+
default
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</button>
|
|
147
|
+
))}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
<div className="my-1 h-px bg-border" />
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={() => {
|
|
154
|
+
setMenuOpen(false)
|
|
155
|
+
setCreateGroupOpen(true)
|
|
156
|
+
}}
|
|
157
|
+
className="block w-full text-left rounded-sm px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent"
|
|
158
|
+
>
|
|
159
|
+
+ create group
|
|
160
|
+
</button>
|
|
161
|
+
<a
|
|
162
|
+
href="/agents"
|
|
163
|
+
className="block rounded-sm px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent"
|
|
164
|
+
>
|
|
165
|
+
manage agents →
|
|
166
|
+
</a>
|
|
167
|
+
<a
|
|
168
|
+
href="/groups"
|
|
169
|
+
className="block rounded-sm px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent"
|
|
170
|
+
>
|
|
171
|
+
manage groups →
|
|
172
|
+
</a>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</header>
|
|
177
|
+
|
|
178
|
+
<nav className="flex-1 overflow-y-auto p-1">
|
|
179
|
+
{agents.length === 0 && groups.length === 0 ? (
|
|
180
|
+
<div className="p-4 text-sm text-muted-foreground">
|
|
181
|
+
No agents yet.{' '}
|
|
182
|
+
<a href="/agents" className="text-primary underline">
|
|
183
|
+
Spawn one
|
|
184
|
+
</a>{' '}
|
|
185
|
+
to start chatting.
|
|
186
|
+
</div>
|
|
187
|
+
) : (
|
|
188
|
+
sortedGroups.map((g) => {
|
|
189
|
+
const groupAgents = agentsByGroup.get(g.id) ?? []
|
|
190
|
+
const containsSelected = selectedGroupId === g.id
|
|
191
|
+
// Explicit user preference wins; otherwise fall back to the
|
|
192
|
+
// "auto-open if selected or the seeded default group" heuristic.
|
|
193
|
+
const stored = openGroups[g.id]
|
|
194
|
+
const isOpen =
|
|
195
|
+
stored !== undefined ? stored : containsSelected || g.id === DEFAULT_GROUP_ID
|
|
196
|
+
return (
|
|
197
|
+
<details
|
|
198
|
+
key={g.id}
|
|
199
|
+
open={isOpen}
|
|
200
|
+
onToggle={(e) => {
|
|
201
|
+
const next = (e.currentTarget as HTMLDetailsElement).open
|
|
202
|
+
if (openGroups[g.id] === next) return
|
|
203
|
+
const merged = { ...openGroups, [g.id]: next }
|
|
204
|
+
// Write the cookie synchronously BEFORE setState so any
|
|
205
|
+
// subsequent navigation/loader-fetch in the same tick sees
|
|
206
|
+
// the new value, and invalidate the route so TanStack
|
|
207
|
+
// Router's cached loader data refreshes from the cookie.
|
|
208
|
+
writeOpenGroupsCookie(merged)
|
|
209
|
+
setOpenGroups(merged)
|
|
210
|
+
void router.invalidate()
|
|
211
|
+
}}
|
|
212
|
+
className="group/details mb-1 last:mb-0"
|
|
213
|
+
>
|
|
214
|
+
<summary className="flex cursor-pointer list-none items-center gap-1.5 rounded-sm px-2 py-1.5 text-xs uppercase tracking-wide text-muted-foreground hover:bg-accent">
|
|
215
|
+
<span className="w-3 text-xs transition-transform group-open/details:rotate-90 inline-block">
|
|
216
|
+
▸
|
|
217
|
+
</span>
|
|
218
|
+
<span className="flex-1 truncate font-semibold">{g.name}</span>
|
|
219
|
+
<span className="font-mono text-xs rounded bg-muted px-1 min-w-[1.4em] text-center">
|
|
220
|
+
{groupAgents.length}
|
|
221
|
+
</span>
|
|
222
|
+
</summary>
|
|
223
|
+
{groupAgents.length === 0 ? (
|
|
224
|
+
<div className="pl-6 py-1 text-xs italic text-muted-foreground">
|
|
225
|
+
no agents
|
|
226
|
+
</div>
|
|
227
|
+
) : (
|
|
228
|
+
<div className="pl-2">
|
|
229
|
+
{groupAgents.map((a) => (
|
|
230
|
+
<div key={a.id} className="group/row relative">
|
|
231
|
+
<Link
|
|
232
|
+
to="/"
|
|
233
|
+
search={{ agent: a.id }}
|
|
234
|
+
className={`block rounded-sm border-l-2 px-3 py-1.5 pr-14 hover:bg-accent ${
|
|
235
|
+
a.id === selectedAgentId
|
|
236
|
+
? 'border-primary bg-accent'
|
|
237
|
+
: 'border-transparent'
|
|
238
|
+
}`}
|
|
239
|
+
>
|
|
240
|
+
<div className="truncate text-sm font-medium">{a.name}</div>
|
|
241
|
+
<div className="flex gap-2 font-mono text-[0.7em] text-muted-foreground">
|
|
242
|
+
<span className="uppercase tracking-wide">{a.status}</span>
|
|
243
|
+
<code>{a.id.slice(0, 8)}</code>
|
|
244
|
+
</div>
|
|
245
|
+
</Link>
|
|
246
|
+
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100">
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => rename(a)}
|
|
250
|
+
title="rename"
|
|
251
|
+
aria-label={`rename ${a.name}`}
|
|
252
|
+
className="unstyled flex h-6 w-6 items-center justify-center rounded-sm text-mocha-light hover:bg-snow hover:text-sapphire"
|
|
253
|
+
>
|
|
254
|
+
✎
|
|
255
|
+
</button>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
onClick={() => archive(a)}
|
|
259
|
+
title="archive"
|
|
260
|
+
aria-label={`archive ${a.name}`}
|
|
261
|
+
className="unstyled flex h-6 w-6 items-center justify-center rounded-sm text-mocha-light hover:bg-snow hover:text-[#9B3D3D]"
|
|
262
|
+
>
|
|
263
|
+
×
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</details>
|
|
271
|
+
)
|
|
272
|
+
})
|
|
273
|
+
)}
|
|
274
|
+
</nav>
|
|
275
|
+
|
|
276
|
+
{spawnFor && (
|
|
277
|
+
<SpawnDialog
|
|
278
|
+
profileId={spawnFor.profileId}
|
|
279
|
+
groupHint={spawnFor.groupHint}
|
|
280
|
+
groups={sortedGroups}
|
|
281
|
+
onClose={() => setSpawnFor(null)}
|
|
282
|
+
/>
|
|
283
|
+
)}
|
|
284
|
+
{createGroupOpen && <CreateGroupDialog onClose={() => setCreateGroupOpen(false)} />}
|
|
285
|
+
</aside>
|
|
286
|
+
)
|
|
287
|
+
}
|