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,153 @@
|
|
|
1
|
+
import { ApiClientError } from '@bazilion/client'
|
|
2
|
+
import type { Agent, Group } from '@bazilion/api-types'
|
|
3
|
+
import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
|
|
4
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { daemonClient } from '../../../lib/daemon-client'
|
|
7
|
+
|
|
8
|
+
interface GroupDetail {
|
|
9
|
+
group: Group
|
|
10
|
+
members: Agent[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const fetchGroup = createServerFn({ method: 'POST' })
|
|
14
|
+
.inputValidator((d: { id: string }) => d)
|
|
15
|
+
.handler(async ({ data }): Promise<GroupDetail | null> => {
|
|
16
|
+
const c = daemonClient()
|
|
17
|
+
let group: Group
|
|
18
|
+
try {
|
|
19
|
+
group = await c.get<Group>(`/api/groups/${encodeURIComponent(data.id)}`)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof ApiClientError && err.status === 404) return null
|
|
22
|
+
throw err
|
|
23
|
+
}
|
|
24
|
+
const all = await c.get<Agent[]>('/api/agents?includeArchived=true')
|
|
25
|
+
const members = all.filter((a) => a.groupId === group.id)
|
|
26
|
+
return { group, members }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const Route = createFileRoute('/groups/$id/')({
|
|
30
|
+
loader: async ({ params }) => {
|
|
31
|
+
const data = await fetchGroup({ data: { id: params.id } })
|
|
32
|
+
if (!data) throw redirect({ to: '/groups' })
|
|
33
|
+
return data
|
|
34
|
+
},
|
|
35
|
+
component: GroupDetailPage,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function GroupDetailPage() {
|
|
39
|
+
const { group, members } = Route.useLoaderData()
|
|
40
|
+
const router = useRouter()
|
|
41
|
+
const [userMd, setUserMd] = useState(group.userMd)
|
|
42
|
+
const [busy, setBusy] = useState(false)
|
|
43
|
+
const [err, setErr] = useState<string | null>(null)
|
|
44
|
+
const [savedAt, setSavedAt] = useState<number | null>(null)
|
|
45
|
+
|
|
46
|
+
async function save() {
|
|
47
|
+
setBusy(true)
|
|
48
|
+
setErr(null)
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`/api/groups/${encodeURIComponent(group.id)}/user-md`, {
|
|
51
|
+
method: 'PUT',
|
|
52
|
+
headers: { 'content-type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ userMd }),
|
|
54
|
+
})
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const e = (await res.json().catch(() => ({}))) as { error?: string }
|
|
57
|
+
throw new Error(e.error ?? `${res.status} ${res.statusText}`)
|
|
58
|
+
}
|
|
59
|
+
setSavedAt(Date.now())
|
|
60
|
+
await router.invalidate()
|
|
61
|
+
} catch (e) {
|
|
62
|
+
setErr((e as Error).message)
|
|
63
|
+
} finally {
|
|
64
|
+
setBusy(false)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<main className="mx-auto max-w-3xl px-6 py-8">
|
|
70
|
+
<h1 className="font-serif text-3xl text-foreground">
|
|
71
|
+
{group.name} <span className="text-muted-foreground text-base">({group.id})</span>
|
|
72
|
+
</h1>
|
|
73
|
+
<p className="text-muted-foreground text-sm mb-6 mt-1">
|
|
74
|
+
<code className="font-mono">{group.path}</code> · {members.length}{' '}
|
|
75
|
+
member{members.length === 1 ? '' : 's'} ·{' '}
|
|
76
|
+
<a
|
|
77
|
+
href={`/groups/${encodeURIComponent(group.id)}/memory`}
|
|
78
|
+
className="text-primary underline"
|
|
79
|
+
>
|
|
80
|
+
shared memory →
|
|
81
|
+
</a>
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<section className="rounded-lg border bg-card p-5 mb-6">
|
|
85
|
+
<h3 className="font-serif text-xl mb-1">USER.md</h3>
|
|
86
|
+
<p className="text-muted-foreground text-sm mb-3">
|
|
87
|
+
Read-only context about the human, injected into every member agent's system prompt.
|
|
88
|
+
12 KB cap. Agents cannot edit this file — only you can.
|
|
89
|
+
</p>
|
|
90
|
+
<textarea
|
|
91
|
+
value={userMd}
|
|
92
|
+
onChange={(e) => setUserMd(e.target.value)}
|
|
93
|
+
rows={16}
|
|
94
|
+
className="w-full rounded-md border bg-background px-3 py-2 font-mono text-sm outline-none focus:ring-2 focus:ring-ring/30"
|
|
95
|
+
placeholder="What the agent should know about you in this group context…"
|
|
96
|
+
/>
|
|
97
|
+
<div className="flex items-center gap-3 mt-3">
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
onClick={save}
|
|
101
|
+
disabled={busy || userMd === group.userMd}
|
|
102
|
+
className="rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50"
|
|
103
|
+
>
|
|
104
|
+
{busy ? 'saving…' : 'save'}
|
|
105
|
+
</button>
|
|
106
|
+
<span className="text-xs text-muted-foreground">
|
|
107
|
+
{userMd.length} / 12000 chars
|
|
108
|
+
</span>
|
|
109
|
+
{savedAt && <span className="text-xs text-emerald-700">✓ saved</span>}
|
|
110
|
+
{err && <span className="text-xs text-rose-700">{err}</span>}
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<section className="rounded-lg border bg-card p-5">
|
|
115
|
+
<h3 className="font-serif text-xl mb-3">members</h3>
|
|
116
|
+
{members.length === 0 ? (
|
|
117
|
+
<p className="text-muted-foreground text-sm">No agents in this group yet.</p>
|
|
118
|
+
) : (
|
|
119
|
+
<table className="w-full text-sm">
|
|
120
|
+
<thead className="text-left text-muted-foreground border-b">
|
|
121
|
+
<tr>
|
|
122
|
+
<th className="py-2">name</th>
|
|
123
|
+
<th>status</th>
|
|
124
|
+
<th>profile</th>
|
|
125
|
+
<th />
|
|
126
|
+
</tr>
|
|
127
|
+
</thead>
|
|
128
|
+
<tbody>
|
|
129
|
+
{members.map((m) => (
|
|
130
|
+
<tr key={m.id} className="border-b last:border-0">
|
|
131
|
+
<td className="py-2">
|
|
132
|
+
<a href={`/agents/${m.id}`} className="text-primary underline">
|
|
133
|
+
{m.name}
|
|
134
|
+
</a>
|
|
135
|
+
</td>
|
|
136
|
+
<td>{m.status}</td>
|
|
137
|
+
<td>
|
|
138
|
+
<code className="font-mono text-xs">{m.profileId}</code>
|
|
139
|
+
</td>
|
|
140
|
+
<td>
|
|
141
|
+
<code className="font-mono text-xs text-muted-foreground">
|
|
142
|
+
{m.id.slice(0, 8)}…
|
|
143
|
+
</code>
|
|
144
|
+
</td>
|
|
145
|
+
</tr>
|
|
146
|
+
))}
|
|
147
|
+
</tbody>
|
|
148
|
+
</table>
|
|
149
|
+
)}
|
|
150
|
+
</section>
|
|
151
|
+
</main>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Per-group shared memory: BM25-indexed markdown notes that every member
|
|
2
|
+
// agent reads from and writes to. Browse the list, search, edit/create
|
|
3
|
+
// entries. The qmd index lives at <group.path>/memory/ on disk.
|
|
4
|
+
|
|
5
|
+
import { ApiClientError } from '@bazilion/client'
|
|
6
|
+
import type { Agent, Group, MemoryEntry, MemoryHit } from '@bazilion/api-types'
|
|
7
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
8
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
9
|
+
import { useEffect, useRef, useState } from 'react'
|
|
10
|
+
import { daemonClient } from '../../../lib/daemon-client'
|
|
11
|
+
|
|
12
|
+
interface MemoryView {
|
|
13
|
+
group: Group
|
|
14
|
+
memberCount: number
|
|
15
|
+
entries: MemoryEntry[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fetchMemory = createServerFn({ method: 'POST' })
|
|
19
|
+
.inputValidator((d: { id: string }) => d)
|
|
20
|
+
.handler(async ({ data }): Promise<MemoryView | null> => {
|
|
21
|
+
const c = daemonClient()
|
|
22
|
+
let group: Group
|
|
23
|
+
try {
|
|
24
|
+
group = await c.get<Group>(`/api/groups/${encodeURIComponent(data.id)}`)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err instanceof ApiClientError && err.status === 404) return null
|
|
27
|
+
throw err
|
|
28
|
+
}
|
|
29
|
+
const [entries, agents] = await Promise.all([
|
|
30
|
+
c.get<MemoryEntry[]>(`/api/groups/${encodeURIComponent(group.id)}/memory`),
|
|
31
|
+
c.get<Agent[]>('/api/agents?includeArchived=true'),
|
|
32
|
+
])
|
|
33
|
+
const memberCount = agents.filter((a) => a.groupId === group.id).length
|
|
34
|
+
return { group, memberCount, entries }
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const Route = createFileRoute('/groups/$id/memory')({
|
|
38
|
+
loader: async ({ params }) => {
|
|
39
|
+
const data = await fetchMemory({ data: { id: params.id } })
|
|
40
|
+
if (!data) throw redirect({ to: '/groups' })
|
|
41
|
+
return data
|
|
42
|
+
},
|
|
43
|
+
component: MemoryPage,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function encodeKey(key: string): string {
|
|
47
|
+
return key.split('/').map(encodeURIComponent).join('/')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ListRow {
|
|
51
|
+
key: string
|
|
52
|
+
preview: string
|
|
53
|
+
score?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type Mode = 'none' | 'edit' | 'new'
|
|
57
|
+
|
|
58
|
+
function MemoryPage() {
|
|
59
|
+
const { group, memberCount, entries: initialEntries } = Route.useLoaderData()
|
|
60
|
+
const groupId = group.id
|
|
61
|
+
|
|
62
|
+
const [rows, setRows] = useState<ListRow[]>(() =>
|
|
63
|
+
initialEntries.map((e) => ({ key: e.key, preview: e.content.slice(0, 80) })),
|
|
64
|
+
)
|
|
65
|
+
const [isSearch, setIsSearch] = useState(false)
|
|
66
|
+
const [query, setQuery] = useState('')
|
|
67
|
+
const [selectedKey, setSelectedKey] = useState<string | null>(null)
|
|
68
|
+
const [mode, setMode] = useState<Mode>('none')
|
|
69
|
+
const [keyInput, setKeyInput] = useState('')
|
|
70
|
+
const [content, setContent] = useState('')
|
|
71
|
+
const [status, setStatus] = useState<{ msg: string; kind: 'info' | 'error' } | null>(null)
|
|
72
|
+
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
73
|
+
|
|
74
|
+
async function loadList(q: string) {
|
|
75
|
+
try {
|
|
76
|
+
const url =
|
|
77
|
+
q.trim().length > 0
|
|
78
|
+
? `/api/groups/${encodeURIComponent(groupId)}/memory/search?q=${encodeURIComponent(q)}&limit=50`
|
|
79
|
+
: `/api/groups/${encodeURIComponent(groupId)}/memory`
|
|
80
|
+
const res = await fetch(url)
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const body = (await res.json().catch(() => null)) as { error?: string } | null
|
|
83
|
+
throw new Error(body?.error ?? res.statusText)
|
|
84
|
+
}
|
|
85
|
+
const data = (await res.json()) as MemoryEntry[] | MemoryHit[]
|
|
86
|
+
if (q.trim().length > 0) {
|
|
87
|
+
setRows(
|
|
88
|
+
(data as MemoryHit[]).map((h) => ({
|
|
89
|
+
key: h.key,
|
|
90
|
+
preview: h.snippet,
|
|
91
|
+
score: h.score,
|
|
92
|
+
})),
|
|
93
|
+
)
|
|
94
|
+
setIsSearch(true)
|
|
95
|
+
} else {
|
|
96
|
+
setRows(
|
|
97
|
+
(data as MemoryEntry[]).map((e) => ({
|
|
98
|
+
key: e.key,
|
|
99
|
+
preview: e.content.slice(0, 80),
|
|
100
|
+
})),
|
|
101
|
+
)
|
|
102
|
+
setIsSearch(false)
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (searchTimer.current) clearTimeout(searchTimer.current)
|
|
111
|
+
searchTimer.current = setTimeout(() => {
|
|
112
|
+
void loadList(query)
|
|
113
|
+
}, 200)
|
|
114
|
+
return () => {
|
|
115
|
+
if (searchTimer.current) clearTimeout(searchTimer.current)
|
|
116
|
+
}
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
}, [query])
|
|
119
|
+
|
|
120
|
+
async function openEntry(key: string) {
|
|
121
|
+
setSelectedKey(key)
|
|
122
|
+
setMode('edit')
|
|
123
|
+
setStatus({ msg: 'loading…', kind: 'info' })
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(
|
|
126
|
+
`/api/groups/${encodeURIComponent(groupId)}/memory/${encodeKey(key)}`,
|
|
127
|
+
)
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const body = (await res.json().catch(() => null)) as { error?: string } | null
|
|
130
|
+
throw new Error(body?.error ?? res.statusText)
|
|
131
|
+
}
|
|
132
|
+
const entry = (await res.json()) as MemoryEntry
|
|
133
|
+
setKeyInput(entry.key)
|
|
134
|
+
setContent(entry.content ?? '')
|
|
135
|
+
setStatus({ msg: `loaded ${entry.key}`, kind: 'info' })
|
|
136
|
+
} catch (err) {
|
|
137
|
+
setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function newEntry() {
|
|
142
|
+
setSelectedKey(null)
|
|
143
|
+
setMode('new')
|
|
144
|
+
setKeyInput('')
|
|
145
|
+
setContent('')
|
|
146
|
+
setStatus({ msg: 'new entry — type a key and content, then save', kind: 'info' })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function save() {
|
|
150
|
+
const key = keyInput.trim()
|
|
151
|
+
if (!key) {
|
|
152
|
+
setStatus({ msg: 'key is required', kind: 'error' })
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(
|
|
157
|
+
`/api/groups/${encodeURIComponent(groupId)}/memory/${encodeKey(key)}`,
|
|
158
|
+
{
|
|
159
|
+
method: 'PUT',
|
|
160
|
+
headers: { 'content-type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({ content }),
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const body = (await res.json().catch(() => null)) as { error?: string } | null
|
|
166
|
+
throw new Error(body?.error ?? res.statusText)
|
|
167
|
+
}
|
|
168
|
+
setSelectedKey(key)
|
|
169
|
+
setMode('edit')
|
|
170
|
+
setStatus({ msg: 'saved', kind: 'info' })
|
|
171
|
+
await loadList(query)
|
|
172
|
+
} catch (err) {
|
|
173
|
+
setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function del() {
|
|
178
|
+
if (!selectedKey) return
|
|
179
|
+
if (!confirm(`delete ${selectedKey}?`)) return
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetch(
|
|
182
|
+
`/api/groups/${encodeURIComponent(groupId)}/memory/${encodeKey(selectedKey)}`,
|
|
183
|
+
{ method: 'DELETE' },
|
|
184
|
+
)
|
|
185
|
+
if (!res.ok && res.status !== 204) {
|
|
186
|
+
const body = (await res.json().catch(() => null)) as { error?: string } | null
|
|
187
|
+
throw new Error(body?.error ?? res.statusText)
|
|
188
|
+
}
|
|
189
|
+
setSelectedKey(null)
|
|
190
|
+
setMode('none')
|
|
191
|
+
setKeyInput('')
|
|
192
|
+
setContent('')
|
|
193
|
+
setStatus({ msg: 'deleted', kind: 'info' })
|
|
194
|
+
await loadList(query)
|
|
195
|
+
} catch (err) {
|
|
196
|
+
setStatus({ msg: `error: ${(err as Error).message}`, kind: 'error' })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const canSave = mode !== 'none' && keyInput.trim().length > 0
|
|
201
|
+
const canDelete = mode === 'edit' && selectedKey !== null
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<main className="mx-auto max-w-5xl px-6 py-8">
|
|
205
|
+
<header className="mb-6">
|
|
206
|
+
<h1 className="font-serif text-3xl text-foreground">
|
|
207
|
+
{group.name}{' '}
|
|
208
|
+
<span className="text-muted-foreground text-base">/ shared memory</span>
|
|
209
|
+
</h1>
|
|
210
|
+
<p className="text-muted-foreground text-sm mt-1">
|
|
211
|
+
BM25-indexed markdown notes shared by every agent in{' '}
|
|
212
|
+
<a
|
|
213
|
+
href={`/groups/${encodeURIComponent(group.id)}`}
|
|
214
|
+
className="font-mono underline"
|
|
215
|
+
>
|
|
216
|
+
{group.id}
|
|
217
|
+
</a>{' '}
|
|
218
|
+
({memberCount} member{memberCount === 1 ? '' : 's'}). Anything written here is
|
|
219
|
+
visible to every member; per-agent notes belong in the agent's own{' '}
|
|
220
|
+
<code className="font-mono">IDENTITY.md</code> via <code>home_write</code>.
|
|
221
|
+
</p>
|
|
222
|
+
</header>
|
|
223
|
+
|
|
224
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr]">
|
|
225
|
+
<div>
|
|
226
|
+
<div className="overflow-hidden rounded-[16px] border border-frost bg-snow">
|
|
227
|
+
<div className="border-b border-frost p-2">
|
|
228
|
+
<input
|
|
229
|
+
type="text"
|
|
230
|
+
placeholder="search (BM25)..."
|
|
231
|
+
autoComplete="off"
|
|
232
|
+
value={query}
|
|
233
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="max-h-[520px] overflow-y-auto">
|
|
237
|
+
{rows.length === 0 ? (
|
|
238
|
+
<div className="p-8 text-center text-mocha-light">no entries</div>
|
|
239
|
+
) : (
|
|
240
|
+
rows.map((r) => {
|
|
241
|
+
const active = r.key === selectedKey
|
|
242
|
+
return (
|
|
243
|
+
<button
|
|
244
|
+
key={r.key}
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={() => openEntry(r.key)}
|
|
247
|
+
className={`unstyled block w-full cursor-pointer border-b border-frost/50 px-3.5 py-2 text-left font-mono text-[0.84em] transition-colors last:border-b-0 hover:bg-sapphire-glow ${
|
|
248
|
+
active ? 'bg-sapphire-glow font-medium text-sapphire-deep' : ''
|
|
249
|
+
}`}
|
|
250
|
+
>
|
|
251
|
+
{isSearch && r.score !== undefined && (
|
|
252
|
+
<span className="float-right text-[0.75em] text-sapphire">
|
|
253
|
+
{r.score.toFixed(2)}
|
|
254
|
+
</span>
|
|
255
|
+
)}
|
|
256
|
+
{r.key}
|
|
257
|
+
{r.preview && (
|
|
258
|
+
<span className="mt-1 block text-[0.9em] font-normal text-mocha-light">
|
|
259
|
+
{r.preview}
|
|
260
|
+
</span>
|
|
261
|
+
)}
|
|
262
|
+
</button>
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<button type="button" className="ghost-btn mt-3 w-full" onClick={newEntry}>
|
|
269
|
+
+ new entry
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div>
|
|
274
|
+
<div className="mb-3 flex items-center gap-2">
|
|
275
|
+
<input
|
|
276
|
+
type="text"
|
|
277
|
+
placeholder="key (e.g. prefs/hiking.md)"
|
|
278
|
+
value={keyInput}
|
|
279
|
+
onChange={(e) => setKeyInput(e.target.value)}
|
|
280
|
+
disabled={mode !== 'new'}
|
|
281
|
+
className="flex-1 font-mono"
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
<textarea
|
|
285
|
+
placeholder="select an entry on the left, or create a new one."
|
|
286
|
+
value={content}
|
|
287
|
+
onChange={(e) => setContent(e.target.value)}
|
|
288
|
+
className="min-h-[360px] w-full font-mono text-[0.9em] leading-[1.55]"
|
|
289
|
+
/>
|
|
290
|
+
<div className="mt-3 flex gap-2">
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
className="btn-primary"
|
|
294
|
+
onClick={save}
|
|
295
|
+
disabled={!canSave}
|
|
296
|
+
>
|
|
297
|
+
save
|
|
298
|
+
</button>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
className="ghost-btn"
|
|
302
|
+
onClick={del}
|
|
303
|
+
disabled={!canDelete}
|
|
304
|
+
>
|
|
305
|
+
delete
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
{status && (
|
|
309
|
+
<p
|
|
310
|
+
className={`mt-2 text-[0.9em] ${
|
|
311
|
+
status.kind === 'error' ? 'text-[#9B3D3D]' : 'text-mocha-light'
|
|
312
|
+
}`}
|
|
313
|
+
>
|
|
314
|
+
{status.msg}
|
|
315
|
+
</p>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</main>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Agent, Group } from '@bazilion/api-types'
|
|
2
|
+
import { createFileRoute, useRouter } from '@tanstack/react-router'
|
|
3
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { daemonClient } from '../../lib/daemon-client'
|
|
6
|
+
|
|
7
|
+
interface GroupsData {
|
|
8
|
+
groups: Group[]
|
|
9
|
+
memberCounts: Record<string, number>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fetchGroupsData = createServerFn({ method: 'GET' }).handler(
|
|
13
|
+
async (): Promise<GroupsData> => {
|
|
14
|
+
const c = daemonClient()
|
|
15
|
+
const [groups, agents] = await Promise.all([
|
|
16
|
+
c.get<Group[]>('/api/groups'),
|
|
17
|
+
c.get<Agent[]>('/api/agents?includeArchived=true'),
|
|
18
|
+
])
|
|
19
|
+
const memberCounts: Record<string, number> = {}
|
|
20
|
+
for (const a of agents) memberCounts[a.groupId] = (memberCounts[a.groupId] ?? 0) + 1
|
|
21
|
+
return { groups, memberCounts }
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const Route = createFileRoute('/groups/')({
|
|
26
|
+
loader: () => fetchGroupsData(),
|
|
27
|
+
component: GroupsPage,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function GroupsPage() {
|
|
31
|
+
const { groups, memberCounts } = Route.useLoaderData()
|
|
32
|
+
const router = useRouter()
|
|
33
|
+
|
|
34
|
+
async function remove(id: string) {
|
|
35
|
+
if (!confirm('remove this group registration?')) return
|
|
36
|
+
const res = await fetch(`/api/groups/${id}`, { method: 'DELETE' })
|
|
37
|
+
if (!res.ok && res.status !== 204) {
|
|
38
|
+
alert(res.statusText)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
await router.invalidate()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<h1>groups</h1>
|
|
47
|
+
<p className="muted">
|
|
48
|
+
A group is a collaboration context: one filesystem root, one USER.md, one roster. Every
|
|
49
|
+
agent belongs to exactly one group.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<RegisterGroupForm onRegistered={() => router.invalidate()} />
|
|
53
|
+
|
|
54
|
+
<table>
|
|
55
|
+
<thead>
|
|
56
|
+
<tr>
|
|
57
|
+
<th>id</th>
|
|
58
|
+
<th>name</th>
|
|
59
|
+
<th>path</th>
|
|
60
|
+
<th>members</th>
|
|
61
|
+
<th>USER.md</th>
|
|
62
|
+
<th />
|
|
63
|
+
</tr>
|
|
64
|
+
</thead>
|
|
65
|
+
<tbody>
|
|
66
|
+
{groups.length === 0 && (
|
|
67
|
+
<tr>
|
|
68
|
+
<td colSpan={6} className="muted">
|
|
69
|
+
no groups registered
|
|
70
|
+
</td>
|
|
71
|
+
</tr>
|
|
72
|
+
)}
|
|
73
|
+
{groups.map((g) => {
|
|
74
|
+
const count = memberCounts[g.id] ?? 0
|
|
75
|
+
const userMdBytes = g.userMd.length
|
|
76
|
+
return (
|
|
77
|
+
<tr key={g.id}>
|
|
78
|
+
<td>
|
|
79
|
+
<code>{g.id}</code>
|
|
80
|
+
</td>
|
|
81
|
+
<td>{g.name}</td>
|
|
82
|
+
<td>
|
|
83
|
+
<code>{g.path}</code>
|
|
84
|
+
</td>
|
|
85
|
+
<td>{count}</td>
|
|
86
|
+
<td>
|
|
87
|
+
<a href={`/groups/${g.id}`}>
|
|
88
|
+
{userMdBytes > 0 ? `${userMdBytes} chars` : 'empty'}
|
|
89
|
+
</a>
|
|
90
|
+
</td>
|
|
91
|
+
<td>
|
|
92
|
+
{count === 0 ? (
|
|
93
|
+
<button type="button" className="ghost-btn" onClick={() => remove(g.id)}>
|
|
94
|
+
remove
|
|
95
|
+
</button>
|
|
96
|
+
) : (
|
|
97
|
+
<span
|
|
98
|
+
className="muted"
|
|
99
|
+
title={`${count} agent(s) belong to this group`}
|
|
100
|
+
>
|
|
101
|
+
in use
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</td>
|
|
105
|
+
</tr>
|
|
106
|
+
)
|
|
107
|
+
})}
|
|
108
|
+
</tbody>
|
|
109
|
+
</table>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function RegisterGroupForm({ onRegistered }: { onRegistered: () => void }) {
|
|
115
|
+
const [id, setId] = useState('')
|
|
116
|
+
const [name, setName] = useState('')
|
|
117
|
+
const [link, setLink] = useState('')
|
|
118
|
+
const [err, setErr] = useState<string | null>(null)
|
|
119
|
+
const [submitting, setSubmitting] = useState(false)
|
|
120
|
+
|
|
121
|
+
async function submit(e: React.FormEvent<HTMLFormElement>) {
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
setErr(null)
|
|
124
|
+
if (!id.trim()) {
|
|
125
|
+
setErr('id is required')
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
setSubmitting(true)
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch('/api/groups', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'content-type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
id: id.trim(),
|
|
135
|
+
name: name.trim() || undefined,
|
|
136
|
+
link: link.trim() || undefined,
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const e2 = (await res.json().catch(() => null)) as { error?: string } | null
|
|
141
|
+
throw new Error(e2?.error ?? res.statusText)
|
|
142
|
+
}
|
|
143
|
+
setId('')
|
|
144
|
+
setName('')
|
|
145
|
+
setLink('')
|
|
146
|
+
onRegistered()
|
|
147
|
+
} catch (e2) {
|
|
148
|
+
setErr((e2 as Error).message)
|
|
149
|
+
} finally {
|
|
150
|
+
setSubmitting(false)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<form className="card" onSubmit={submit}>
|
|
156
|
+
<h3>register group</h3>
|
|
157
|
+
{err && <div className="err">{err}</div>}
|
|
158
|
+
<p className="muted">
|
|
159
|
+
Groups always live under <code>~/.bazilion/groups/<slug>/</code>. Leave the link
|
|
160
|
+
target blank to create a fresh directory; supply an absolute path to materialize the slot
|
|
161
|
+
as a symlink to your existing project tree instead.
|
|
162
|
+
</p>
|
|
163
|
+
<div className="flex gap-4">
|
|
164
|
+
<label className="flex-1">
|
|
165
|
+
id (slug)
|
|
166
|
+
<input
|
|
167
|
+
value={id}
|
|
168
|
+
onChange={(e) => setId(e.target.value)}
|
|
169
|
+
required
|
|
170
|
+
placeholder="myproject"
|
|
171
|
+
/>
|
|
172
|
+
</label>
|
|
173
|
+
<label className="flex-1">
|
|
174
|
+
name
|
|
175
|
+
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="My Project" />
|
|
176
|
+
</label>
|
|
177
|
+
</div>
|
|
178
|
+
<label>
|
|
179
|
+
link target (optional, absolute path)
|
|
180
|
+
<input
|
|
181
|
+
value={link}
|
|
182
|
+
onChange={(e) => setLink(e.target.value)}
|
|
183
|
+
placeholder="/home/user/projects/myproject"
|
|
184
|
+
/>
|
|
185
|
+
</label>
|
|
186
|
+
<button type="submit" disabled={submitting}>
|
|
187
|
+
{submitting ? 'registering…' : 'register'}
|
|
188
|
+
</button>
|
|
189
|
+
</form>
|
|
190
|
+
)
|
|
191
|
+
}
|