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.
Files changed (260) hide show
  1. package/.understand-anything/.understandignore +25 -0
  2. package/.understand-anything/fingerprints.json +14267 -0
  3. package/.understand-anything/knowledge-graph.json +18128 -0
  4. package/.understand-anything/meta.json +6 -0
  5. package/CLAUDE.md +164 -0
  6. package/LICENSE +21 -0
  7. package/README.md +195 -0
  8. package/apps/cli/package.json +21 -0
  9. package/apps/cli/src/auth-file.ts +18 -0
  10. package/apps/cli/src/client.ts +37 -0
  11. package/apps/cli/src/columnize.ts +32 -0
  12. package/apps/cli/src/commands/agent.ts +574 -0
  13. package/apps/cli/src/commands/auth.ts +110 -0
  14. package/apps/cli/src/commands/backup.ts +135 -0
  15. package/apps/cli/src/commands/completion.ts +155 -0
  16. package/apps/cli/src/commands/config.ts +95 -0
  17. package/apps/cli/src/commands/doctor.ts +131 -0
  18. package/apps/cli/src/commands/group.ts +132 -0
  19. package/apps/cli/src/commands/inbox.ts +82 -0
  20. package/apps/cli/src/commands/login.ts +73 -0
  21. package/apps/cli/src/commands/memory.ts +106 -0
  22. package/apps/cli/src/commands/profile.ts +259 -0
  23. package/apps/cli/src/commands/provider.ts +170 -0
  24. package/apps/cli/src/commands/send.ts +24 -0
  25. package/apps/cli/src/commands/serve.ts +89 -0
  26. package/apps/cli/src/commands/skill.ts +120 -0
  27. package/apps/cli/src/commands/token.ts +148 -0
  28. package/apps/cli/src/commands/trigger.ts +129 -0
  29. package/apps/cli/src/commands/uninstall.ts +156 -0
  30. package/apps/cli/src/index.ts +196 -0
  31. package/apps/cli/src/paths.ts +12 -0
  32. package/apps/cli/test/agent.test.ts +213 -0
  33. package/apps/cli/test/backup.test.ts +95 -0
  34. package/apps/cli/test/chat.test.ts +539 -0
  35. package/apps/cli/test/columnize.test.ts +29 -0
  36. package/apps/cli/test/completion.test.ts +45 -0
  37. package/apps/cli/test/config-page.test.ts +151 -0
  38. package/apps/cli/test/group.test.ts +74 -0
  39. package/apps/cli/test/helpers.ts +70 -0
  40. package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
  41. package/apps/cli/test/inbox.test.ts +147 -0
  42. package/apps/cli/test/memory.test.ts +69 -0
  43. package/apps/cli/test/profile.test.ts +110 -0
  44. package/apps/cli/test/send.test.ts +30 -0
  45. package/apps/cli/test/server-fixture.ts +212 -0
  46. package/apps/cli/test/session-head.test.ts +45 -0
  47. package/apps/cli/test/skill.test.ts +128 -0
  48. package/apps/cli/test/token.test.ts +109 -0
  49. package/apps/cli/test/trigger.test.ts +245 -0
  50. package/apps/cli/tsconfig.json +4 -0
  51. package/apps/daemon/package.json +31 -0
  52. package/apps/daemon/src/app.ts +41 -0
  53. package/apps/daemon/src/core/agent/archive.ts +8 -0
  54. package/apps/daemon/src/core/agent/delete.ts +30 -0
  55. package/apps/daemon/src/core/agent/resolve.ts +31 -0
  56. package/apps/daemon/src/core/agent/spawn.ts +95 -0
  57. package/apps/daemon/src/core/agent/unarchive.ts +11 -0
  58. package/apps/daemon/src/core/availableModels.ts +58 -0
  59. package/apps/daemon/src/core/db/client.ts +113 -0
  60. package/apps/daemon/src/core/db/migrate.ts +41 -0
  61. package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
  62. package/apps/daemon/src/core/group/delete.ts +21 -0
  63. package/apps/daemon/src/core/group/register.ts +68 -0
  64. package/apps/daemon/src/core/index.ts +71 -0
  65. package/apps/daemon/src/core/paths.ts +56 -0
  66. package/apps/daemon/src/core/profile/create.ts +78 -0
  67. package/apps/daemon/src/core/profile/delete.ts +26 -0
  68. package/apps/daemon/src/core/profile/identity.ts +70 -0
  69. package/apps/daemon/src/core/profile/load.ts +45 -0
  70. package/apps/daemon/src/core/profile/seed.ts +74 -0
  71. package/apps/daemon/src/core/profile/templates.ts +60 -0
  72. package/apps/daemon/src/core/profile/update.ts +57 -0
  73. package/apps/daemon/src/core/profile/validate.ts +9 -0
  74. package/apps/daemon/src/core/repos/agents.ts +202 -0
  75. package/apps/daemon/src/core/repos/config.ts +78 -0
  76. package/apps/daemon/src/core/repos/groups.ts +55 -0
  77. package/apps/daemon/src/core/repos/messages.ts +127 -0
  78. package/apps/daemon/src/core/repos/profiles.ts +83 -0
  79. package/apps/daemon/src/core/repos/providerModels.ts +58 -0
  80. package/apps/daemon/src/core/repos/providerState.ts +37 -0
  81. package/apps/daemon/src/core/repos/secrets.ts +145 -0
  82. package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
  83. package/apps/daemon/src/core/repos/triggers.ts +101 -0
  84. package/apps/daemon/src/core/repos/webTokens.ts +87 -0
  85. package/apps/daemon/src/core/secrets.ts +65 -0
  86. package/apps/daemon/src/core/services.ts +264 -0
  87. package/apps/daemon/src/core/skills/discover.ts +28 -0
  88. package/apps/daemon/src/core/skills/import.ts +136 -0
  89. package/apps/daemon/src/core/skills/parse.ts +52 -0
  90. package/apps/daemon/src/core/skills/resolve.ts +50 -0
  91. package/apps/daemon/src/index.ts +45 -0
  92. package/apps/daemon/src/lib/agent-cancel.ts +48 -0
  93. package/apps/daemon/src/lib/agent-id.ts +13 -0
  94. package/apps/daemon/src/lib/agent-turn.ts +56 -0
  95. package/apps/daemon/src/lib/api-key.ts +56 -0
  96. package/apps/daemon/src/lib/auth.ts +41 -0
  97. package/apps/daemon/src/lib/cron.ts +93 -0
  98. package/apps/daemon/src/lib/ctx.ts +80 -0
  99. package/apps/daemon/src/lib/messaging-host.ts +34 -0
  100. package/apps/daemon/src/lib/middleware-auth.ts +52 -0
  101. package/apps/daemon/src/lib/scheduler.ts +294 -0
  102. package/apps/daemon/src/routes/agents.ts +772 -0
  103. package/apps/daemon/src/routes/auth-login.ts +193 -0
  104. package/apps/daemon/src/routes/config.ts +267 -0
  105. package/apps/daemon/src/routes/groups.ts +133 -0
  106. package/apps/daemon/src/routes/messages.ts +29 -0
  107. package/apps/daemon/src/routes/misc.ts +239 -0
  108. package/apps/daemon/src/routes/profiles.ts +197 -0
  109. package/apps/daemon/src/routes/skills.ts +123 -0
  110. package/apps/daemon/src/routes/triggers.ts +29 -0
  111. package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
  112. package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
  113. package/apps/daemon/src/runtime/index.ts +77 -0
  114. package/apps/daemon/src/runtime/memory/files.ts +103 -0
  115. package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
  116. package/apps/daemon/src/runtime/memory/types.ts +16 -0
  117. package/apps/daemon/src/runtime/pi/events.ts +173 -0
  118. package/apps/daemon/src/runtime/pi/session.ts +536 -0
  119. package/apps/daemon/src/runtime/pi/tools.ts +85 -0
  120. package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
  121. package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
  122. package/apps/daemon/src/runtime/providers/registry.ts +374 -0
  123. package/apps/daemon/src/runtime/providers/retry.ts +176 -0
  124. package/apps/daemon/src/runtime/providers/types.ts +33 -0
  125. package/apps/daemon/src/runtime/session/prompt.ts +83 -0
  126. package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
  127. package/apps/daemon/src/runtime/tools/home.ts +114 -0
  128. package/apps/daemon/src/runtime/tools/memory.ts +81 -0
  129. package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
  130. package/apps/daemon/src/runtime/tools/registry.ts +29 -0
  131. package/apps/daemon/src/runtime/tools/types.ts +13 -0
  132. package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
  133. package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
  134. package/apps/daemon/src/runtime/tools/web.ts +273 -0
  135. package/apps/daemon/src/runtime/worker/entry.ts +221 -0
  136. package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
  137. package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
  138. package/apps/daemon/test/core/agents.test.ts +319 -0
  139. package/apps/daemon/test/core/available-models.test.ts +63 -0
  140. package/apps/daemon/test/core/config.test.ts +99 -0
  141. package/apps/daemon/test/core/groups.test.ts +63 -0
  142. package/apps/daemon/test/core/helpers.ts +50 -0
  143. package/apps/daemon/test/core/identity.test.ts +85 -0
  144. package/apps/daemon/test/core/migrations.test.ts +83 -0
  145. package/apps/daemon/test/core/profiles.test.ts +182 -0
  146. package/apps/daemon/test/core/provider-models.test.ts +57 -0
  147. package/apps/daemon/test/core/provider-state.test.ts +36 -0
  148. package/apps/daemon/test/core/skill-meta.test.ts +45 -0
  149. package/apps/daemon/test/core/skills.test.ts +271 -0
  150. package/apps/daemon/test/core/triggers.test.ts +182 -0
  151. package/apps/daemon/test/core/web-tokens.test.ts +79 -0
  152. package/apps/daemon/test/cron.test.ts +90 -0
  153. package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
  154. package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
  155. package/apps/daemon/test/runtime/memory.test.ts +78 -0
  156. package/apps/daemon/test/runtime/messaging.test.ts +213 -0
  157. package/apps/daemon/test/runtime/mock-server.ts +65 -0
  158. package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
  159. package/apps/daemon/test/runtime/providers.test.ts +306 -0
  160. package/apps/daemon/test/runtime/retry.test.ts +191 -0
  161. package/apps/daemon/test/runtime/session-head.test.ts +90 -0
  162. package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
  163. package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
  164. package/apps/daemon/tsconfig.json +4 -0
  165. package/apps/mobile/README.md +60 -0
  166. package/apps/mobile/app/_layout.tsx +58 -0
  167. package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
  168. package/apps/mobile/app/agents/[id]/index.tsx +166 -0
  169. package/apps/mobile/app/agents/index.tsx +212 -0
  170. package/apps/mobile/app/index.tsx +21 -0
  171. package/apps/mobile/app/pair.tsx +226 -0
  172. package/apps/mobile/app/settings.tsx +419 -0
  173. package/apps/mobile/app.json +49 -0
  174. package/apps/mobile/assets/adaptive-icon.png +0 -0
  175. package/apps/mobile/assets/favicon.png +0 -0
  176. package/apps/mobile/assets/icon.png +0 -0
  177. package/apps/mobile/assets/splash-icon.png +0 -0
  178. package/apps/mobile/babel.config.js +6 -0
  179. package/apps/mobile/metro.config.js +28 -0
  180. package/apps/mobile/package.json +44 -0
  181. package/apps/mobile/src/auth.ts +66 -0
  182. package/apps/mobile/src/pair-url.ts +48 -0
  183. package/apps/mobile/src/theme-context.tsx +88 -0
  184. package/apps/mobile/src/theme.ts +135 -0
  185. package/apps/mobile/test/pair-url.test.ts +46 -0
  186. package/apps/mobile/tsconfig.json +23 -0
  187. package/apps/web/components.json +25 -0
  188. package/apps/web/package.json +39 -0
  189. package/apps/web/public/baziu.svg +8 -0
  190. package/apps/web/src/components/AgentTabs.tsx +45 -0
  191. package/apps/web/src/components/BaziuLogo.tsx +21 -0
  192. package/apps/web/src/components/ChatPane.tsx +1033 -0
  193. package/apps/web/src/components/ConfigTabs.tsx +29 -0
  194. package/apps/web/src/components/CopyButton.tsx +68 -0
  195. package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
  196. package/apps/web/src/components/FieldRow.tsx +94 -0
  197. package/apps/web/src/components/Footer.tsx +10 -0
  198. package/apps/web/src/components/PawIcon.tsx +15 -0
  199. package/apps/web/src/components/Sidebar.tsx +287 -0
  200. package/apps/web/src/components/SpawnDialog.tsx +129 -0
  201. package/apps/web/src/components/ThemeToggle.tsx +75 -0
  202. package/apps/web/src/components/TopNav.tsx +34 -0
  203. package/apps/web/src/components/ui/button.tsx +67 -0
  204. package/apps/web/src/components/ui/card.tsx +103 -0
  205. package/apps/web/src/components/ui/checkbox.tsx +31 -0
  206. package/apps/web/src/components/ui/dialog.tsx +168 -0
  207. package/apps/web/src/components/ui/input.tsx +19 -0
  208. package/apps/web/src/components/ui/label.tsx +22 -0
  209. package/apps/web/src/components/ui/radio-group.tsx +44 -0
  210. package/apps/web/src/components/ui/select.tsx +192 -0
  211. package/apps/web/src/components/ui/separator.tsx +26 -0
  212. package/apps/web/src/components/ui/table.tsx +116 -0
  213. package/apps/web/src/components/ui/tabs.tsx +88 -0
  214. package/apps/web/src/components/ui/textarea.tsx +18 -0
  215. package/apps/web/src/lib/auth.ts +50 -0
  216. package/apps/web/src/lib/daemon-client.ts +34 -0
  217. package/apps/web/src/lib/md.ts +45 -0
  218. package/apps/web/src/lib/utils.ts +6 -0
  219. package/apps/web/src/lib/wire-constants.ts +27 -0
  220. package/apps/web/src/routeTree.gen.ts +408 -0
  221. package/apps/web/src/router.tsx +20 -0
  222. package/apps/web/src/routes/__root.tsx +123 -0
  223. package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
  224. package/apps/web/src/routes/agents/$id/index.tsx +527 -0
  225. package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
  226. package/apps/web/src/routes/agents/index.tsx +265 -0
  227. package/apps/web/src/routes/api/$.ts +88 -0
  228. package/apps/web/src/routes/config/index.tsx +315 -0
  229. package/apps/web/src/routes/config/services.tsx +49 -0
  230. package/apps/web/src/routes/config/tokens.tsx +192 -0
  231. package/apps/web/src/routes/groups/$id/index.tsx +153 -0
  232. package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
  233. package/apps/web/src/routes/groups/index.tsx +191 -0
  234. package/apps/web/src/routes/index.tsx +133 -0
  235. package/apps/web/src/routes/login.tsx +63 -0
  236. package/apps/web/src/routes/profiles/$id.tsx +549 -0
  237. package/apps/web/src/routes/profiles/index.tsx +458 -0
  238. package/apps/web/src/routes/skills/index.tsx +297 -0
  239. package/apps/web/src/routes/welcome.tsx +61 -0
  240. package/apps/web/src/styles.css +449 -0
  241. package/apps/web/tsconfig.json +25 -0
  242. package/apps/web/vite.config.ts +25 -0
  243. package/biome.json +23 -0
  244. package/docs/agent-engine.md +219 -0
  245. package/docs/architecture.md +627 -0
  246. package/docs/backlog/README.md +42 -0
  247. package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
  248. package/docs/openclaw-reference.md +210 -0
  249. package/package.json +38 -0
  250. package/packages/api-types/package.json +11 -0
  251. package/packages/api-types/src/entities.ts +146 -0
  252. package/packages/api-types/src/events.ts +55 -0
  253. package/packages/api-types/src/index.ts +488 -0
  254. package/packages/api-types/src/memory.ts +15 -0
  255. package/packages/client/package.json +13 -0
  256. package/packages/client/src/index.ts +117 -0
  257. package/pnpm-workspace.yaml +3 -0
  258. package/tsconfig.base.json +23 -0
  259. package/tsconfig.json +11 -0
  260. 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/&lt;slug&gt;/</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
+ }