From 34418fb510a293aa6991f51de599f6fc7e5a2e5a Mon Sep 17 00:00:00 2001 From: Patrizio Rullo Date: Mon, 25 May 2026 17:55:07 +0200 Subject: [PATCH] Scaffold apps/tui workspace + BAZ-004 user story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New TS + Ink 7 + React 19 TUI client, packaged via `bun build --compile`. Reuses @bazilion/client and @bazilion/api-types directly (the hermetic- package investment pays off). v1 scope is scaffold + read-mostly screens — agents-list lands here, streaming chat and write CRUD follow in per-screen BAZs. Dev runs via `tsx` (matches monorepo convention); only the release path needs Bun. apps/tui is excluded from the root tsconfig (JSX + needs `jsx: "react-jsx"`, same precedent as web/mobile) but stays inside biome.json since it follows root conventions. User story moved to docs/backlog/todo/ with the full framework decision (10-agent fan-out research summary) and v1 scope. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/tui/package.json | 27 ++ apps/tui/src/app.tsx | 31 +++ apps/tui/src/auth-file.ts | 18 ++ apps/tui/src/client.ts | 38 +++ apps/tui/src/index.tsx | 14 + apps/tui/src/paths.ts | 12 + apps/tui/src/screens/agents-list.tsx | 97 +++++++ apps/tui/tsconfig.json | 8 + docs/backlog/README.md | 6 +- docs/backlog/todo/BAZ-004-tui-client.md | 206 +++++++++++++++ pnpm-lock.yaml | 324 +++++++++++++++++++++++- 11 files changed, 765 insertions(+), 16 deletions(-) create mode 100644 apps/tui/package.json create mode 100644 apps/tui/src/app.tsx create mode 100644 apps/tui/src/auth-file.ts create mode 100644 apps/tui/src/client.ts create mode 100644 apps/tui/src/index.tsx create mode 100644 apps/tui/src/paths.ts create mode 100644 apps/tui/src/screens/agents-list.tsx create mode 100644 apps/tui/tsconfig.json create mode 100644 docs/backlog/todo/BAZ-004-tui-client.md diff --git a/apps/tui/package.json b/apps/tui/package.json new file mode 100644 index 0000000..1a773a0 --- /dev/null +++ b/apps/tui/package.json @@ -0,0 +1,27 @@ +{ + "name": "@bazilion/tui", + "version": "0.0.0", + "private": true, + "description": "Terminal UI for the Bazilion daemon — Ink + React, packaged via `bun build --compile`. See docs/backlog/draft/BAZ-004-tui-client.md.", + "type": "module", + "scripts": { + "dev": "tsx src/index.tsx", + "typecheck": "tsc --noEmit", + "compile:darwin-arm64": "bun build src/index.tsx --compile --target=bun-darwin-arm64 --outfile dist/bazi-darwin-arm64", + "compile:darwin-x64": "bun build src/index.tsx --compile --target=bun-darwin-x64 --outfile dist/bazi-darwin-x64", + "compile:linux-x64": "bun build src/index.tsx --compile --target=bun-linux-x64 --outfile dist/bazi-linux-x64", + "compile:linux-arm64": "bun build src/index.tsx --compile --target=bun-linux-arm64 --outfile dist/bazi-linux-arm64", + "compile:windows-x64": "bun build src/index.tsx --compile --target=bun-windows-x64 --outfile dist/bazi-windows-x64.exe" + }, + "dependencies": { + "@bazilion/api-types": "workspace:*", + "@bazilion/client": "workspace:*", + "cli-highlight": "^2.1.11", + "ink": "^7.0.4", + "marked": "^14.1.4", + "react": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19.2.15" + } +} diff --git a/apps/tui/src/app.tsx b/apps/tui/src/app.tsx new file mode 100644 index 0000000..6dea5c7 --- /dev/null +++ b/apps/tui/src/app.tsx @@ -0,0 +1,31 @@ +import { Box, Text, useApp, useInput } from 'ink' +import { type ClientConfig, createClient } from './client.ts' +import { AgentsList } from './screens/agents-list.tsx' + +interface AppProps { + config: ClientConfig +} + +export function App({ config }: AppProps) { + const client = createClient(config) + const { exit } = useApp() + + useInput((input, key) => { + if (input === 'q' || (key.ctrl && input === 'c')) exit() + }) + + return ( + + + + bazi + + · {config.serverUrl} + + + + q · quit + + + ) +} diff --git a/apps/tui/src/auth-file.ts b/apps/tui/src/auth-file.ts new file mode 100644 index 0000000..526f74d --- /dev/null +++ b/apps/tui/src/auth-file.ts @@ -0,0 +1,18 @@ +import { existsSync, readFileSync } from 'node:fs' + +export interface AuthFile { + token: string + remote?: { server: string; token: string } | null +} + +export function readAuthFile(authFile: string): AuthFile { + if (!existsSync(authFile)) { + throw new Error(`${authFile} not found. Start the daemon with \`bazilion serve\` first.`) + } + const raw = readFileSync(authFile, 'utf8') + const parsed = JSON.parse(raw) as Partial + if (typeof parsed.token !== 'string' || !parsed.token) { + throw new Error(`${authFile} is missing the "token" field`) + } + return { token: parsed.token, remote: parsed.remote ?? null } +} diff --git a/apps/tui/src/client.ts b/apps/tui/src/client.ts new file mode 100644 index 0000000..a240b3d --- /dev/null +++ b/apps/tui/src/client.ts @@ -0,0 +1,38 @@ +import { type BazilionClient, createClient as createPackageClient } from '@bazilion/client' +import { readAuthFile } from './auth-file.ts' +import { resolveTuiPaths } from './paths.ts' + +export type { BazilionClient } from '@bazilion/client' +export { ApiClientError } from '@bazilion/client' + +export interface ClientConfig { + serverUrl: string + token: string +} + +// Mirrors `apps/cli/src/client.ts:loadClientConfig` deliberately — the TUI and +// CLI share the same fallback chain (env → auth.remote → loopback) so a user +// who paired their CLI against a remote daemon gets the TUI pointed at the +// same place. We don't import from apps/cli to keep app-to-app coupling out. +export function loadClientConfig(): ClientConfig { + const envUrl = process.env.BAZILION_SERVER + const envToken = process.env.BAZILION_TOKEN + if (envUrl && envToken) return { serverUrl: envUrl, token: envToken } + + const paths = resolveTuiPaths() + const auth = readAuthFile(paths.authFile) + if (auth.remote?.server && auth.remote.token) { + return { + serverUrl: envUrl ?? auth.remote.server, + token: auth.remote.token, + } + } + return { + serverUrl: envUrl ?? 'http://127.0.0.1:4321', + token: auth.token, + } +} + +export function createClient(cfg: ClientConfig = loadClientConfig()): BazilionClient { + return createPackageClient({ serverUrl: cfg.serverUrl, token: cfg.token }) +} diff --git a/apps/tui/src/index.tsx b/apps/tui/src/index.tsx new file mode 100644 index 0000000..2c8dfaa --- /dev/null +++ b/apps/tui/src/index.tsx @@ -0,0 +1,14 @@ +import { render } from 'ink' +import { App } from './app.tsx' +import { type ClientConfig, loadClientConfig } from './client.ts' + +let config: ClientConfig +try { + config = loadClientConfig() +} catch (err) { + const msg = err instanceof Error ? err.message : String(err) + process.stderr.write(`bazi: ${msg}\n`) + process.exit(1) +} + +render() diff --git a/apps/tui/src/paths.ts b/apps/tui/src/paths.ts new file mode 100644 index 0000000..b14457c --- /dev/null +++ b/apps/tui/src/paths.ts @@ -0,0 +1,12 @@ +import { homedir } from 'node:os' +import { join } from 'node:path' + +export interface TuiPaths { + home: string + authFile: string +} + +export function resolveTuiPaths(home?: string): TuiPaths { + const root = home ?? process.env.BAZILION_HOME ?? join(homedir(), '.bazilion') + return { home: root, authFile: join(root, 'auth.json') } +} diff --git a/apps/tui/src/screens/agents-list.tsx b/apps/tui/src/screens/agents-list.tsx new file mode 100644 index 0000000..23b8e1e --- /dev/null +++ b/apps/tui/src/screens/agents-list.tsx @@ -0,0 +1,97 @@ +import type { Agent } from '@bazilion/api-types' +import { Box, Text } from 'ink' +import { useEffect, useState } from 'react' +import type { BazilionClient } from '../client.ts' +import { ApiClientError } from '../client.ts' + +interface AgentsListProps { + client: BazilionClient +} + +type FetchState = + | { kind: 'loading' } + | { kind: 'ok'; agents: Agent[] } + | { kind: 'setup-required' } + | { kind: 'error'; message: string } + +export function AgentsList({ client }: AgentsListProps) { + const [state, setState] = useState({ kind: 'loading' }) + + useEffect(() => { + let cancelled = false + client + .get('/api/agents') + .then((agents) => { + if (!cancelled) setState({ kind: 'ok', agents }) + }) + .catch((err: unknown) => { + if (cancelled) return + // 409 from any data endpoint means the first-run setup gate hasn't + // cleared yet — finish provider + model curation in the web UI or via + // the CLI, then relaunch. Handled distinctly from generic errors so + // the user gets actionable next steps. + if (err instanceof ApiClientError && err.status === 409) { + setState({ kind: 'setup-required' }) + return + } + setState({ kind: 'error', message: err instanceof Error ? err.message : String(err) }) + }) + return () => { + cancelled = true + } + }, [client]) + + if (state.kind === 'loading') return Loading agents… + + if (state.kind === 'setup-required') { + return ( + + Setup not complete. + + Enable a provider and curate at least one model via the web UI (http://127.0.0.1:4322) or + the CLI, then relaunch. + + + ) + } + + if (state.kind === 'error') { + return ( + + Failed to load agents: + {state.message} + + ) + } + + if (state.agents.length === 0) { + return No agents yet. Spawn one with `bazilion agent spawn`. + } + + return ( + + Agents ({state.agents.length}) + {state.agents.map((agent) => ( + + + {agent.name} + + {' '} + {agent.modelOverride ?? 'profile-default'} · {agent.id.slice(0, 8)} + + + ))} + + ) +} + +function statusColor(status: Agent['status']): string { + switch (status) { + case 'running': + return 'green' + case 'archived': + return 'gray' + default: + return 'cyan' + } +} diff --git a/apps/tui/tsconfig.json b/apps/tui/tsconfig.json new file mode 100644 index 0000000..42bc36b --- /dev/null +++ b/apps/tui/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx"] +} diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 5cdd537..ce3731a 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -34,9 +34,11 @@ docs/backlog/ | [BAZ-001](draft/BAZ-001-a2a-federation-spike.md) | Spike — federated multi-employee Bazilion via A2A | S | Investigation only; output is a follow-up implementation BAZ | | [BAZ-003](draft/BAZ-003-hermes-self-learning.md) | Hermes-style self-learning loop — background reviewer + skill self-editing | L | MVP = reviewer + human-approval gate; curator / FTS5 / runtime skill authoring deferred to v2 BAZs | -## Todo (0) +## Todo (1) -_None right now._ +| ID | Title | Size | Notes | +|----|-------|------|-------| +| [BAZ-004](todo/BAZ-004-tui-client.md) | TUI client (apps/tui) — feature-parity terminal UI for the daemon | L | TS + Ink + React, packaged via `bun build --compile`; reuses `@bazilion/client` + `@bazilion/api-types`. v1 = scaffold + read-mostly screens. Scaffold landed; v1 screens to follow | ## Done (1) diff --git a/docs/backlog/todo/BAZ-004-tui-client.md b/docs/backlog/todo/BAZ-004-tui-client.md new file mode 100644 index 0000000..0ecf31f --- /dev/null +++ b/docs/backlog/todo/BAZ-004-tui-client.md @@ -0,0 +1,206 @@ +--- +id: BAZ-004 +title: TUI client (apps/tui) — feature-parity terminal UI for the daemon +status: todo +size: L (≈1–2 weeks for v1; scaffolding lands in a day, full parity is staged) +created: 2026-05-25 +refined: 2026-05-25 +note: New workspace `apps/tui` built on Ink + React + TS; reuses `@bazilion/client` and `@bazilion/api-types` directly. Distribution via `bun build --compile` per-platform. Strictly additive — no changes to daemon, web, mobile, or CLI. +--- + +# BAZ-004 — TUI client (apps/tui) + +**Status:** Backlog (draft). Today Bazilion ships two end-user surfaces: the CLI (`apps/cli`, citty-based, command-per-action) and the web UI (`apps/web`, TanStack Start, full multi-screen SPA). The CLI is great for scripting and one-shot operations but is awkward for a long live session — listing agents and then jumping into a chat means typing two unrelated commands; watching an inbox means polling by hand. The web UI fits browser-native users but requires running a Vite dev server on port 4322 alongside the daemon on 4321, which is friction for terminal-native operators who already live in tmux. This BAZ adds `apps/tui` — a single-binary terminal UI that opens directly against the local (or paired remote) daemon and gives you the same surfaces the web app exposes, with no Vite server, no browser, and the same auth as the CLI (loopback bearer from `~/.bazilion/auth.json`). + +**Dependency:** None. Sits entirely on top of the existing HTTP surface; the daemon, CLI, web, and mobile apps are untouched. The TUI is a fourth client. + +**Framework decision (resolved 2026-05-25 via 10-agent fan-out research):** TypeScript + [Ink](https://github.com/vadimdemedes/ink) (React for terminals) bundled with `bun build --compile`. The deciding factor was code reuse — `packages/client` and `packages/api-types` were explicitly built hermetic (no `node:*` imports) so any TS client gets the wire layer for free; every other stack (Go+Bubble Tea, Rust+Ratatui, Python+Textual, etc.) would have required hand-porting ~30 entity/event types and maintaining them on every wire-shape change with no compile-time link to the source-of-truth. Trade-off accepted: ~80–100 MB single-file binary (vs ~15 MB for Go, ~5 MB for Rust) because it embeds the Bun runtime — acceptable for a developer-facing client where the bottleneck is HTTP I/O, not render throughput. Proof-of-life: Claude Code, GitHub Copilot CLI, Codex CLI all ship Ink + a runtime-embedded binary in production. Full evaluation notes archived in the conversation history that produced this BAZ. + +## User stories + +- **As a terminal-native operator**, I want one binary I can launch in any pane (`bazi`) that connects to my local daemon via `~/.bazilion/auth.json` and drops me into a navigable, multi-screen UI, so I don't need to context-switch to a browser tab and don't need to run `apps/web`'s Vite server alongside the daemon. +- **As an operator paired against a remote daemon (Tailscale / LAN)**, I want to launch the TUI against a remote daemon by pasting a `bazilion://pair?server=…&token=…` URL or passing `--server …` / `--token …`, so a remote-host Bazilion instance is as ergonomic as the local one. +- **As an operator deep in a chat with one of my agents**, I want the streaming `assistant_delta` frames to render token-by-token inside the chat view with markdown + syntax-highlighted code blocks, so the TUI feels as live as the web UI does today — not "log a line per chunk". +- **As an operator watching multiple agents**, I want the agent-list screen to show unread-inbox counts, currently-running indicator, and last-activity timestamp, so I can see at a glance which agent wants my attention. +- **As a release-engineer publishing the TUI**, I want one `pnpm --filter @bazilion/tui release` workflow to produce signed single-file binaries for darwin-arm64, darwin-x64, linux-x64, linux-arm64, and windows-x64 via `bun build --compile`, so distribution doesn't depend on the user's having Node/Bun/pnpm installed. + +## Goal + +Ship `apps/tui` — a new workspace that produces a single executable per platform giving feature parity with `apps/web` for the surfaces an operator actually uses interactively. "Feature parity" is scoped (see Scope below): the parity bar is the *interactive* surfaces (chat, agent list, inbox, triggers, config), not every form-edit-button the web has. The TUI does NOT replace the CLI for scripting (one-shot commands stay in `apps/cli`) and does NOT replace the web (which keeps being the canonical configuration surface for end-users who prefer a browser). + +**Strict additivity:** the existing CLI's commands and the existing web routes stay exactly as they are. The TUI is a new client of the daemon, not a refactor of anything. + +## Why now + +Three pressures converge: + +1. **CLI/web parity is mandatory per `CLAUDE.md`, but the CLI is not actually parity for interactive flows.** The CLI shines for `bazilion serve`, `bazilion agent spawn`, `bazilion inbox list ` and similar one-shots. The web UI handles everything else. There is no terminal-native option for "I want to chat with my agent without leaving my terminal" — `bazilion agent chat ` exists but it's a one-shot blocking call inside a shell, not a live persistent UI. The TUI closes that gap without requiring a browser. +2. **The hermetic-package investment pays off best when we have a third TS client.** `packages/api-types` and `packages/client` were deliberately built `node:`-free precisely to support clients outside the daemon process. `apps/cli` and `apps/mobile` already reuse them; `apps/tui` is the third user and the one that benefits most (full TS App with React + Hooks, not a thin command wrapper or a mobile experiment). +3. **Mobile pairing infrastructure exists and is unused on desktop.** `bazilion://pair?…` and `bazilion token create --qr` ship today for `apps/mobile`. The TUI can reuse the same pairing URL parser (it's already a unit-tested pure TS module at `apps/mobile/src/pair-url.ts`) to let users pair against a remote daemon by pasting the URL — no extra protocol work. + +Doing this now also gives us a clean place to dogfood streaming-chat UI patterns (delta batching, scrollback virtualization, markdown re-render) that the web UI also wrestles with, before next-generation features (richer tool-call rendering, inline diff approvals from `BAZ-003`) ossify a different shape. + +## Scope + +### v1 — scaffold + read-only parity (in this BAZ) + +Lands a working `apps/tui` workspace with the framework in place and the read-mostly screens implemented. Write/CRUD screens stub to a "switch to web" prompt initially and fill in over follow-up BAZs. + +**Workspace structure:** + +``` +apps/tui/ + package.json # @bazilion/tui, private, bin: bazi + tsconfig.json # extends root base, adds jsx: 'react-jsx' + src/ + index.tsx # entry — load creds, render + paths.ts # mirrors apps/cli/src/paths.ts (resolveTuiPaths) + auth-file.ts # mirrors apps/cli/src/auth-file.ts (readAuthFile) + client.ts # loadClientConfig — same env-vars + auth.json fallback chain as CLI + pair-url.ts # copy of apps/mobile/src/pair-url.ts (or extracted to packages/pair later) + app.tsx # top-level — owns route state + sidebar + router.tsx # screen enum + dispatcher + screens/ + agents-list.tsx + agent-chat.tsx + agent-inbox.tsx + agent-triggers.tsx + profiles-list.tsx + groups-list.tsx + config.tsx + welcome.tsx # first-run gate handler (catches 409s) + components/ + sidebar.tsx + statusbar.tsx + spinner.tsx + markdown.tsx # marked + cli-highlight → Ink tree + hooks/ + use-agents.ts # `useQuery`-shaped wrapper over client.get + use-chat-stream.ts # owns the NDJSON consumer + delta batcher + test/ + pair-url.test.ts # copied from apps/mobile, kept independent + markdown.test.ts # markdown renderer snapshots +``` + +**v1 screens (parity scope):** + +| Screen | Web route | TUI behavior | +|---|---|---| +| Agents list | `/agents` | List with name, group, model, status badge, unread-inbox count. Enter → chat. | +| Agent chat | `/agents/:id` | Streaming chat view (NDJSON `ChatFrame` → markdown render with `marked` + `cli-highlight`). `/` slash-command palette for compact/reset/cancel. | +| Agent inbox | `/agents/:id/inbox` | Read-only list initially. Send-message form lands in v1.1. | +| Agent triggers | `/agents/:id/triggers` | Read-only list initially. CRUD lands in v1.1. | +| Profiles list | `/profiles` | Read-only list with default-model + skill mode. Edit punts to "open in web". | +| Groups list | `/groups` | Read-only list. USER.md / memory views punt to web. | +| Config / providers | `/config` | Read-only: which providers are enabled + their curated models + their hasKey state. Editing punts to web ("Press w to open in browser"). | +| Welcome | `/welcome` | Triggered by a 409 from any data screen. Tells the operator to finish setup in the web UI (or via CLI), then press `r` to retry. | + +**v1 NOT in scope (deferred to v1.1 / later BAZs):** + +- Profile editor (SOUL.md / IDENTITY.md / BOOTSTRAP.md / AGENTS.md / TOOLS.md / HEARTBEAT.md textareas). +- Provider config (enabling providers, entering API keys, curating models). +- Skills install / import. +- Group USER.md editing. +- Per-group memory search and write. +- Profile-group templates editor + spawn. +- ChatGPT OAuth flow (`POST /auth/openai/login` requires loopback browser flow — same caveat as the CLI; surface link to the web for now). +- Theme switcher (ship with one good default; theming lands when there's demand). + +### Auth model + +Same fallback chain as the CLI (`apps/cli/src/client.ts:loadClientConfig`): + +1. `BAZILION_SERVER` + `BAZILION_TOKEN` env vars win when both are set. +2. Otherwise read `~/.bazilion/auth.json` and use `auth.remote` if present (remote pairing target), else `auth.token` against `http://127.0.0.1:4321`. +3. CLI flags `--server ` / `--token ` override both (for ad-hoc testing). + +`bazilion://pair?…` URL handling: TUI accepts the URL via `--pair ` flag OR `bazi pair` interactive prompt that parses the URL via `apps/mobile/src/pair-url.ts` (copied initially; consider extracting to `packages/pair-url` if a third consumer appears). Successful pair writes `auth.remote = {server, token}` into `~/.bazilion/auth.json` — matching what `bazilion login` does today. + +### Streaming chat (the hardest piece) + +`POST /api/agents/:id/chat` returns NDJSON `ChatFrame`s via `@bazilion/client`'s `stream()` async generator (already implemented at [packages/client/src/index.ts](../../../packages/client/src/index.ts)). The TUI's `use-chat-stream.ts` hook: + +- Calls `client.stream('POST', `/api/agents/${id}/chat`, {message})` and pushes frames into a `useReducer` state. +- **Coalesces `assistant_delta` frames** into a `requestAnimationFrame`-equivalent batch (60ms window) to avoid React-thrashing the terminal — chat models can emit 200+ frames/sec, and re-rendering a 1000-line scrollback per token will flicker. +- **Partitions the chat view into two render zones**: finished messages live under Ink's `` (rendered once, never re-rendered) + the active in-flight assistant message is the only dynamic component. This is the same pattern Claude Code uses and is the difference between "fluid" and "stuttering". +- Wires Ctrl+C / `q` to abort: closes the response body (which triggers the daemon's `/cancel` agentId-keyed AbortController on its own — no extra HTTP call needed when the operator just wants to stop watching). +- Surfaces `kind:'fatal'` frames as an error toast; surfaces `tool_call` / `tool_result` as collapsed-by-default detail blocks the user can expand with `Enter`. + +### Markdown rendering + +`marked` + `cli-highlight` is the boring correct choice (mirrors what `apps/web`'s [lib/md.ts](../../../apps/web/src/lib/md.ts) does conceptually, since both apps use `marked`). DOMPurify is not needed — there's no DOM. Output is folded into an Ink `` tree with ANSI colors. Known Windows-Terminal tokenizing issue with `cli-highlight` is real (substrings inside identifiers mis-colored) — ship the workaround of `--no-highlight` flag for users hitting it; the highlight tax for the rest is small. + +### Packaging — `bun build --compile` + +Builds per platform run via `pnpm --filter @bazilion/tui compile:`. Targets: + +- `bun-darwin-arm64` +- `bun-darwin-x64` +- `bun-linux-x64` +- `bun-linux-arm64` +- `bun-windows-x64` + +Output: `apps/tui/dist/bazi-` (~80–100 MB each). The release pipeline (a follow-up CI workflow) runs all five in parallel matrix, hashes each, publishes them as GitHub Release assets on tag push. Bun is pinned in CI; **devs do not need Bun installed locally** — `pnpm dev` and `pnpm typecheck` use `tsx` (consistent with the rest of the monorepo). Only the release pipeline needs Bun. + +The `bin` field in `apps/tui/package.json` points at the tsx entry for `pnpm dlx`-style local invocation; the compiled binary is the actual end-user artifact and is published outside npm (GitHub Releases + later Homebrew tap + Scoop bucket). + +### Tooling exceptions + +- `apps/tui` is **excluded from the root `tsconfig.json`** (mirrors `apps/web` / `apps/mobile`) because it uses JSX and needs `jsx: "react-jsx"` which the root base doesn't set. Run `pnpm --filter @bazilion/tui typecheck` for TUI-only TS checks. The whole-tree `typecheck` script intentionally skips it (same precedent as web/mobile). +- `apps/tui` **stays inside `biome.json`'s default include** — unlike `apps/web` and `apps/mobile`, it follows root conventions (single quotes, no semicolons, 2-space, 100-col) and Biome handles `.tsx` natively. No need to opt out. +- `apps/tui` is **excluded from the root `vitest run`** by directory globbing today, since Ink components need their own renderer. Add `apps/tui/vitest.config.ts` if/when component tests land; for v1 the unit tests are pure-TS (pair-url, markdown) and can run under the root vitest. + +## Out of scope + +- **TUI-side OAuth flows.** ChatGPT OAuth (`POST /auth/openai/login`) needs a loopback HTTP server + browser handoff. The CLI handles this with citty + qrcode-terminal; the TUI's first version will surface a "press `w` to log in via web" prompt instead of re-implementing the flow inside Ink. Revisit if user demand appears. +- **A TUI-native config editor.** Provider enable/disable + API-key entry + model curation are configuration ceremonies; doing them once via the web UI (or CLI) is fine. The TUI surfaces *status* but not *editing* in v1. +- **Skill import / management.** Same reasoning — install-once operation, fine to do via CLI or web. +- **Per-group memory write / search UI.** The shared-memory backend works fine, but a polished editor + search-result-with-snippets renderer is a separate piece of work. v1 shows the memory entries list as read-only. +- **Profile / profile-group editor.** v1 read-only. +- **Theme system.** Ship one good default — Charm-style palette. Add theming when a second theme is actually requested. +- **Replacing the CLI.** `apps/cli` keeps existing for scripting / one-shot ops. The TUI is an *additional* interactive surface, not a replacement. `bazilion agent spawn`, `bazilion serve`, `bazilion token create` stay in the CLI. +- **Web-feature catch-up.** Anything that doesn't have a v1 row in the screens table (above) is explicitly punted. Don't grow scope here; spawn a new BAZ. + +## Open questions + +1. **Binary name: `bazi` or `bazilion-tui` or `bz`?** Leaning `bazi` — short enough for muscle memory, distinct from `bazilion` (the CLI), doesn't collide with `bz` (which is taken by a few tools). Decide before publishing. +2. **Single binary or `bazilion tui` subcommand?** Could pack the TUI as a subcommand of the existing CLI rather than a separate binary. Pros: one install. Cons: explodes the CLI's `pnpm dlx bazilion` size by ~80 MB; couples release cadences. Leaning separate binary; revisit if users ask. +3. **Static markdown footnotes vs link-in-status-bar for clickable URLs?** TUI can't make links truly clickable across all terminals. Initial answer: render link as underlined coloured text with the URL appended in dim parentheses inline; OSC-8 hyperlinks supported in modern terminals (iTerm2, Kitty, WezTerm, Windows Terminal recent) as a progressive enhancement. +4. **Sidebar always-visible vs collapsible?** Leaning always-visible at ≥120-col, auto-collapsed below. Confirm after building it. +5. **Should the pair-url parser move to a shared `packages/pair-url`?** Three consumers (mobile, TUI, future browser SPA) would justify extracting. v1: just copy from mobile (10 LOC, zero deps); extract on the third consumer. + +## Decisions (resolved 2026-05-25) + +1. **Stack:** TypeScript + Ink + React, packaged via `bun build --compile`. (See "Framework decision" in the header note for the why.) +2. **Dev runtime is `tsx`, not Bun.** Matches the rest of the monorepo's `pnpm tsx …` convention. Bun is only required in the release pipeline. Devs don't need Bun installed to develop the TUI. +3. **Reuse `@bazilion/client` and `@bazilion/api-types` directly** — no wire-type re-declaration anywhere in `apps/tui`. +4. **Markdown:** `marked` + `cli-highlight`. Output folded into Ink `` tree. +5. **NDJSON consumption:** use `@bazilion/client`'s existing `stream()` generator. Coalesce `assistant_delta` into 60ms batches, partition chat view via `` for finished messages. +6. **Mirror, don't import, CLI's local `paths.ts` + `auth-file.ts`.** They're ~10 LOC each; cross-app imports between `apps/cli` and `apps/tui` would be awkward and CLAUDE.md's pragmatic-exception rule is scoped to CLI ↔ daemon, not app ↔ app. +7. **v1 ships read-mostly; write CRUD lands per-screen over v1.1/v1.2** (separate BAZs as scope demands). The framework is the load-bearing piece; getting it solid is more valuable than rushing 17 half-implemented forms. + +## Tests + +- **Pure-TS unit tests** (run under root `pnpm test`): + - `apps/tui/test/pair-url.test.ts` — pair URL parsing (copy / port from `apps/mobile/test/pair-url.test.ts`). + - `apps/tui/test/markdown.test.ts` — markdown renderer snapshots for: headings, lists, fenced code with language, links, inline code, blockquotes. + - `apps/tui/test/use-chat-stream.test.ts` — the delta-coalescing reducer (pure logic, no Ink). +- **Ink-rendered smoke tests** (per-app `vitest` config, lands when first component test is needed): + - `apps/tui/test/screens/agents-list.test.tsx` — render with a mocked client returning a small fixture, assert the list shows the names + statuses. + - `apps/tui/test/screens/agent-chat.test.tsx` — feed a scripted async-iterator of `ChatFrame`s into `use-chat-stream`, assert the rendered output ends with the accumulated message text. +- **Manual / scripted acceptance:** + - `pnpm --filter @bazilion/tui dev` against a running local daemon: agent list → chat → send message → watch streaming output → cancel mid-stream → see "cancelled" tool-error frame. Repeat against a remote (Tailscale) daemon paired via `bazi pair `. + - `pnpm --filter @bazilion/tui compile:darwin-arm64` produces a binary; running it from a clean shell with no Node/Bun/pnpm in PATH reaches the daemon and renders the agent list. +- **First-run regression:** with a fresh `~/.bazilion/` (deleted), launch the TUI → expect a clear "daemon not running, start with `bazilion serve`" error (not a stack trace, not a hang). Then start the daemon → relaunch → expect the welcome screen (because setup-gate 409 will fire on `/api/agents`) → finish setup via web → press `r` → expect the agent list. +- **TS check parity:** `pnpm --filter @bazilion/tui typecheck` runs cleanly. The root `pnpm typecheck` continues to ignore `apps/tui` (mirroring web/mobile precedent) and remains green. +- **Biome:** `pnpm lint` continues to pass with `apps/tui` included (no opt-out, unlike web/mobile). + +## Deliverable + +A new `apps/tui` workspace producing a TypeScript + Ink + React app that: + +- Launches against the local daemon by default (or a remote one via env vars, `--server`/`--token` flags, or `bazi pair `). +- Renders the v1 screen set (agent list, chat with streaming, inbox/triggers/profiles/groups/config as read-only). +- Compiles to a single per-platform binary via `bun build --compile`. +- Has its own per-app `typecheck` script, stays inside the root `biome.json` and root `vitest.config.ts` for pure-TS unit tests. +- Does not modify `apps/daemon`, `apps/web`, `apps/mobile`, `apps/cli`, or any `packages/*`. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd80150..c732c84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,31 @@ importers: specifier: ^6.0.3 version: 6.0.3 + apps/tui: + dependencies: + '@bazilion/api-types': + specifier: workspace:* + version: link:../../packages/api-types + '@bazilion/client': + specifier: workspace:* + version: link:../../packages/client + cli-highlight: + specifier: ^2.1.11 + version: 2.1.11 + ink: + specifier: ^7.0.4 + version: 7.0.4(@types/react@19.2.15)(react-devtools-core@6.1.5)(react@19.2.6) + marked: + specifier: ^14.1.4 + version: 14.1.4 + react: + specifier: ^19.2.6 + version: 19.2.6 + devDependencies: + '@types/react': + specifier: ^19.2.15 + version: 19.2.15 + apps/web: dependencies: '@bazilion/api-types': @@ -315,6 +340,10 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@alcalzone/ansi-tokenize@0.3.0': + resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} + engines: {node: '>=18'} + '@anthropic-ai/sdk@0.91.1': resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} hasBin: true @@ -3509,6 +3538,10 @@ packages: resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==} engines: {node: '>=14.16'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -3586,6 +3619,10 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -3826,14 +3863,27 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-boxes@4.0.1: + resolution: {integrity: sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==} + engines: {node: '>=18.20 <19 || >=20.10'} + cli-cursor@2.1.0: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} engines: {node: '>=4'} + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -3842,6 +3892,10 @@ packages: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} + cli-truncate@6.0.0: + resolution: {integrity: sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==} + engines: {node: '>=22'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -3849,6 +3903,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3869,6 +3926,10 @@ packages: code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -3946,6 +4007,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} @@ -4218,6 +4283,10 @@ packages: resolution: {integrity: sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==} engines: {node: '>=10'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -4239,6 +4308,9 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-toolkit@1.47.0: + resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} @@ -4260,6 +4332,10 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -4644,10 +4720,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} - engines: {node: '>=18'} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -4841,12 +4913,29 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ink@7.0.4: + resolution: {integrity: sha512-4wsM/gMKOT2ZANNTJibI6I9IcwBfobqv/CgaDcwvOaCREZIQxo3iGQS7qPHa2hmA67NYltZWCMtBDELB/mcbJQ==} + engines: {node: '>=22'} + peerDependencies: + '@types/react': '>=19.2.0' + react: '>=19.2.0' + react-devtools-core: '>=6.1.2' + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + inline-style-prefixer@7.0.1: resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} @@ -4906,6 +4995,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -5264,6 +5358,11 @@ packages: resolution: {integrity: sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==} hasBin: true + marked@14.1.4: + resolution: {integrity: sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==} + engines: {node: '>= 18'} + hasBin: true + marked@15.0.12: resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} engines: {node: '>= 18'} @@ -5773,12 +5872,21 @@ packages: resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} engines: {node: '>=10'} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} parse5-parser-stream@7.1.2: resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -5789,6 +5897,10 @@ packages: partial-json@0.1.7: resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -6121,6 +6233,12 @@ packages: '@types/react': optional: true + react-reconciler@0.33.0: + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.0 + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -6232,6 +6350,10 @@ packages: resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} engines: {node: '>=4'} + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -6421,6 +6543,10 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + slice-ansi@9.0.0: + resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==} + engines: {node: '>=22'} + slugify@1.6.9: resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==} engines: {node: '>=8.0.0'} @@ -6487,6 +6613,10 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -6653,6 +6783,10 @@ packages: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + terser@5.46.2: resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==} engines: {node: '>=10'} @@ -7118,6 +7252,14 @@ packages: engines: {node: '>=8'} hasBin: true + widest-line@6.0.0: + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} + engines: {node: '>=20'} + + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -7193,10 +7335,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -7209,6 +7359,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -7232,6 +7385,11 @@ snapshots: '@adobe/css-tools@4.5.0': {} + '@alcalzone/ansi-tokenize@0.3.0': + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + '@anthropic-ai/sdk@0.91.1(zod@4.2.1)': dependencies: json-schema-to-ts: 3.1.1 @@ -10162,8 +10320,8 @@ snapshots: dependencies: '@react-native/js-polyfills': 0.85.3 '@react-native/metro-babel-transformer': 0.85.3(@babel/core@7.29.0) - metro-config: 0.84.3 - metro-runtime: 0.84.3 + metro-config: 0.84.4 + metro-runtime: 0.84.4 transitivePeerDependencies: - '@babel/core' - supports-color @@ -10186,7 +10344,7 @@ snapshots: '@react-navigation/routers': 7.5.3 escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 - nanoid: 3.3.11 + nanoid: 3.3.12 query-string: 7.1.3 react: 19.2.6 react-is: 19.2.5 @@ -10210,14 +10368,14 @@ snapshots: '@react-navigation/core': 7.17.2(react@19.2.6) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 - nanoid: 3.3.11 + nanoid: 3.3.12 react: 19.2.6 react-native: 0.85.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3(@babel/core@7.29.0))(@types/react@19.2.15)(react@19.2.6) use-latest-callback: 0.2.6(react@19.2.6) '@react-navigation/routers@7.5.3': dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 '@reflink/reflink-darwin-arm64@0.1.19': optional: true @@ -10945,6 +11103,10 @@ snapshots: ansi-escapes@6.2.1: {} + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} @@ -11004,6 +11166,8 @@ snapshots: dependencies: retry: 0.13.1 + auto-bind@5.0.1: {} + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.0 @@ -11325,22 +11489,48 @@ snapshots: dependencies: clsx: 2.1.1 + cli-boxes@4.0.1: {} + cli-cursor@2.1.0: dependencies: restore-cursor: 2.0.0 + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + cli-spinners@2.9.2: {} cli-spinners@3.4.0: {} + cli-truncate@6.0.0: + dependencies: + slice-ansi: 9.0.0 + string-width: 8.2.0 + cli-width@4.1.0: {} client-only@0.0.1: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -11367,6 +11557,10 @@ snapshots: code-block-writer@13.0.3: {} + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -11438,6 +11632,8 @@ snapshots: convert-source-map@2.0.0: {} + convert-to-spaces@2.0.1: {} + cookie-es@3.1.1: {} cookie-signature@1.2.2: {} @@ -11657,6 +11853,8 @@ snapshots: env-var@7.5.0: {} + environment@1.1.0: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -11675,6 +11873,8 @@ snapshots: dependencies: es-errors: 1.3.0 + es-toolkit@1.47.0: {} + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -11739,6 +11939,8 @@ snapshots: escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} esprima@4.0.1: {} @@ -12218,8 +12420,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} - get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -12407,10 +12607,47 @@ snapshots: indent-string@4.0.0: {} + indent-string@5.0.0: {} + inherits@2.0.4: {} ini@1.3.8: {} + ink@7.0.4(@types/react@19.2.15)(react-devtools-core@6.1.5)(react@19.2.6): + dependencies: + '@alcalzone/ansi-tokenize': 0.3.0 + ansi-escapes: 7.3.0 + ansi-styles: 6.2.3 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 4.0.1 + cli-cursor: 4.0.0 + cli-truncate: 6.0.0 + code-excerpt: 4.0.0 + es-toolkit: 1.47.0 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.6 + react-reconciler: 0.33.0(react@19.2.6) + scheduler: 0.27.0 + signal-exit: 3.0.7 + slice-ansi: 9.0.0 + stack-utils: 2.0.6 + string-width: 8.2.0 + terminal-size: 4.0.1 + type-fest: 5.6.0 + widest-line: 6.0.0 + wrap-ansi: 10.0.0 + ws: 8.20.0 + yoga-layout: 3.2.1 + optionalDependencies: + '@types/react': 19.2.15 + react-devtools-core: 6.1.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + inline-style-prefixer@7.0.1: dependencies: css-in-js-utils: 3.1.0 @@ -12469,12 +12706,14 @@ snapshots: is-fullwidth-code-point@5.1.0: dependencies: - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-in-ci@2.0.0: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -12768,6 +13007,8 @@ snapshots: mdurl: 1.0.1 uc.micro: 1.0.6 + marked@14.1.4: {} + marked@15.0.12: {} marked@18.0.4: {} @@ -13513,6 +13754,10 @@ snapshots: dependencies: pngjs: 3.4.0 + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -13522,6 +13767,10 @@ snapshots: dependencies: parse5: 7.3.0 + parse5@5.1.1: {} + + parse5@6.0.1: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -13530,6 +13779,8 @@ snapshots: partial-json@0.1.7: {} + patch-console@2.0.0: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -13969,6 +14220,11 @@ snapshots: - supports-color - utf-8-validate + react-reconciler@0.33.0(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.15)(react@19.2.6): @@ -14077,6 +14333,11 @@ snapshots: onetime: 2.0.1 signal-exit: 3.0.7 + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -14373,6 +14634,11 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + slice-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + slugify@1.6.9: {} source-map-js@1.2.1: {} @@ -14422,6 +14688,10 @@ snapshots: srvx@0.11.15: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stackback@0.0.2: {} stackframe@1.3.4: {} @@ -14464,12 +14734,12 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 string-width@8.2.0: dependencies: - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 string_decoder@1.3.0: @@ -14579,6 +14849,8 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 + terminal-size@4.0.1: {} + terser@5.46.2: dependencies: '@jridgewell/source-map': 0.3.11 @@ -14944,6 +15216,16 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@6.0.0: + dependencies: + string-width: 8.2.0 + + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.0 + strip-ansi: 7.2.0 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -14992,8 +15274,20 @@ snapshots: yaml@2.9.0: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -15010,6 +15304,8 @@ snapshots: yoctocolors@2.1.2: {} + yoga-layout@3.2.1: {} + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76