Add TUI launcher implementation and project docs
Source modules for history parsing, git metadata, project scanning, terminal launching, and OpenTUI component layout. Remove private flag for publishing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
287
CLAUDE.md
287
CLAUDE.md
@@ -1,111 +1,222 @@
|
||||
---
|
||||
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||
alwaysApply: false
|
||||
---
|
||||
# cladm
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
Interactive terminal UI for launching Claude Code sessions across project folders. Built with OpenTUI + Bun.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
## Project Overview
|
||||
|
||||
## APIs
|
||||
A rich TUI app that replaces `~/Desktop/launch-claude.sh`. Scans `~/.claude/history.jsonl` to discover projects, displays them in an interactive multi-select list with metadata (git branch, last commit, dirty status, Claude session stats), and launches selected projects in new Terminal windows with `claude --dangerously-skip-permissions`.
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
## Tech Stack
|
||||
|
||||
## Testing
|
||||
- **Runtime**: Bun (>=1.3.0) — use `bun` exclusively, never node/npm/npx
|
||||
- **UI Framework**: @opentui/core (imperative API, not React/Solid bindings)
|
||||
- **Language**: TypeScript
|
||||
|
||||
Use `bun test` to run tests.
|
||||
## Architecture
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
src/
|
||||
index.ts — Entry point: createCliRenderer, compose layout, input loop
|
||||
data/
|
||||
history.ts — Parse ~/.claude/history.jsonl → project list with sessions/msgs/timestamps
|
||||
git.ts — Git metadata: branch, last commit, dirty status (staged/unstaged/untracked)
|
||||
scanner.ts — Filesystem fallback scanner (recursive, skips node_modules etc.)
|
||||
components/
|
||||
project-list.ts — ScrollBox + selectable rows with checkbox toggles
|
||||
header.ts — Title bar with selected count, sort mode, source label
|
||||
footer.ts — Keybinding hints
|
||||
row.ts — Single project row: checkbox, name, branch, commit, dirty, claude stats, stack tags
|
||||
actions/
|
||||
launcher.ts — osascript Terminal.app integration to open claude sessions
|
||||
sorter.ts — Sort logic: recent claude, name, last commit, most sessions
|
||||
lib/
|
||||
colors.ts — Theme colors and highlight helpers
|
||||
time.ts — Relative time formatting (time_ago)
|
||||
```
|
||||
|
||||
## Frontend
|
||||
## Run
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
```sh
|
||||
bun run src/index.ts
|
||||
```
|
||||
|
||||
Server:
|
||||
## Key Behaviors
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
1. **Default mode**: reads `~/.claude/history.jsonl` to discover all projects (sorted by most recently used)
|
||||
2. **Fallback**: recursive filesystem scan from `~/Desktop` if no history
|
||||
3. **Interactive picker**: arrow keys navigate, space toggles, `a` all, `n` none, `s` cycles sort, enter launches, `q` quits
|
||||
4. **Per-row metadata**: project name (relative path), git branch, last commit age + message, dirty status (+staged ~modified ?untracked), last Claude use, session count, message count, stack tags
|
||||
5. **Launch**: each selected project opens a new Terminal.app window via osascript running `cd <path> && claude --dangerously-skip-permissions`
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
## OpenTUI Reference
|
||||
|
||||
### Quick Start Pattern
|
||||
```typescript
|
||||
import { createCliRenderer, Box, Text, ScrollBox, Select, Input } from "@opentui/core"
|
||||
|
||||
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
||||
renderer.root.add(
|
||||
Box({ flexDirection: "column", width: "100%", height: "100%" },
|
||||
// children...
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Core Concepts
|
||||
- **Constructs** are factory functions: `Box(props, ...children)` → returns VNode
|
||||
- **Renderables** are class instances: `new BoxRenderable(renderer, options)`
|
||||
- Use constructs for declarative composition, renderables when you need imperative control
|
||||
- `renderer.root.add(vnode)` to mount, `.remove(id)` to unmount
|
||||
- `requestRender()` to trigger re-draw after state changes
|
||||
|
||||
### Layout (Yoga/Flexbox)
|
||||
All components accept flexbox props:
|
||||
- `flexDirection: "row" | "column" | "row-reverse" | "column-reverse"`
|
||||
- `justifyContent: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"`
|
||||
- `alignItems: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"`
|
||||
- `width/height`: number (chars) or string ("100%", "50%")
|
||||
- `padding`, `paddingTop/Right/Bottom/Left`, `margin*`, `gap`, `rowGap`, `columnGap`
|
||||
- `position: "relative" | "absolute"` with `top/right/bottom/left`
|
||||
- `overflow: "visible" | "hidden" | "scroll"`
|
||||
- `flexGrow`, `flexShrink`, `flexBasis`
|
||||
|
||||
### Box
|
||||
```typescript
|
||||
Box({
|
||||
borderStyle: "single" | "double" | "rounded" | "heavy",
|
||||
border: boolean | ("top" | "right" | "bottom" | "left")[],
|
||||
borderColor: string | RGBA,
|
||||
backgroundColor: string | RGBA,
|
||||
focusedBorderColor: ColorInput,
|
||||
title: string,
|
||||
titleAlignment: "left" | "center" | "right",
|
||||
shouldFill: boolean,
|
||||
focusable: boolean,
|
||||
gap: number | `${number}%`,
|
||||
}, ...children)
|
||||
```
|
||||
|
||||
### Text + Styled Text
|
||||
```typescript
|
||||
import { Text, t, bold, italic, underline, dim, fg, bg, strikethrough } from "@opentui/core"
|
||||
|
||||
Text({
|
||||
content: "plain" | t`${bold(fg("#00FFFF")("styled"))}`,
|
||||
fg: string | RGBA,
|
||||
bg: string | RGBA,
|
||||
wrapMode: "none" | "char" | "word",
|
||||
truncate: boolean,
|
||||
selectable: boolean,
|
||||
attributes: number, // TextAttributes bitmask
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
Color helpers: `red()`, `green()`, `blue()`, `yellow()`, `cyan()`, `magenta()`, `white()`, `gray()`, `brightRed()`, etc.
|
||||
Background: `bgRed()`, `bgGreen()`, `bgBlue()`, `bgYellow()`, etc.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
### Input
|
||||
```typescript
|
||||
Input({
|
||||
placeholder: string,
|
||||
maxLength: number, // default 1000
|
||||
value: string, // initial value
|
||||
// Inherits TextareaOptions for colors, keybindings
|
||||
})
|
||||
// Events: "input" (each char), "change" (on blur), "enter" (on Enter key)
|
||||
// Methods: .value, .focus(), .blur(), .submit()
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
### Select
|
||||
```typescript
|
||||
Select({
|
||||
options: [{ name: string, description: string, value?: any }],
|
||||
backgroundColor / textColor / focusedBackgroundColor / focusedTextColor,
|
||||
selectedBackgroundColor / selectedTextColor,
|
||||
descriptionColor / selectedDescriptionColor,
|
||||
showScrollIndicator: boolean,
|
||||
wrapSelection: boolean,
|
||||
showDescription: boolean,
|
||||
itemSpacing: number,
|
||||
fastScrollStep: number,
|
||||
keyBindings: SelectKeyBinding[],
|
||||
})
|
||||
// Events: "selectionChanged", "itemSelected"
|
||||
// Methods: .getSelectedOption(), .getSelectedIndex(), .moveUp/Down(), .selectCurrent()
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
### ScrollBox
|
||||
```typescript
|
||||
ScrollBox({
|
||||
scrollX: boolean, // default false
|
||||
scrollY: boolean, // default true
|
||||
stickyScroll: boolean,
|
||||
viewportCulling: boolean, // default true
|
||||
// Inherits BoxOptions (border, bg, etc.)
|
||||
})
|
||||
// Methods: .scrollBy(delta), .scrollTo(pos), .scrollTop/.scrollLeft
|
||||
// Children added via .add() are delegated to internal content container
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
### Keyboard Handling
|
||||
```typescript
|
||||
// Global input handler
|
||||
renderer.addInputHandler((key: KeyEvent) => {
|
||||
key.name // "a", "return", "escape", "up", "down", etc.
|
||||
key.ctrl // boolean
|
||||
key.meta // boolean (Cmd on mac)
|
||||
key.shift // boolean
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
})
|
||||
|
||||
// Per-component
|
||||
Box({ onKeyDown: (key) => { ... }, focusable: true })
|
||||
```
|
||||
|
||||
### Colors (RGBA)
|
||||
```typescript
|
||||
import { RGBA } from "@opentui/core"
|
||||
RGBA.fromHex("#FF0000")
|
||||
RGBA.fromInts(255, 0, 0, 255)
|
||||
RGBA.fromValues(1.0, 0.0, 0.0, 1.0)
|
||||
```
|
||||
|
||||
### Renderer Lifecycle
|
||||
```typescript
|
||||
const renderer = await createCliRenderer({
|
||||
exitOnCtrlC: true,
|
||||
targetFps: 30,
|
||||
useMouse: true,
|
||||
useAlternateScreen: true,
|
||||
onDestroy: () => { /* cleanup */ },
|
||||
})
|
||||
// renderer.start() — auto-called
|
||||
// renderer.destroy() — cleanup and exit
|
||||
```
|
||||
|
||||
### Animation
|
||||
```typescript
|
||||
import { createTimeline } from "@opentui/core"
|
||||
const tl = createTimeline()
|
||||
tl.add({ targets: myRenderable, duration: 500, ease: "outQuad", onUpdate: (anim) => { ... } })
|
||||
```
|
||||
|
||||
### Key Patterns for This Project
|
||||
- Use `Box` with `flexDirection: "column"` for the main layout (header, list, footer)
|
||||
- Use `ScrollBox` for the project list (handles overflow + scrolling)
|
||||
- Build each row as a `Box` with `flexDirection: "row"` containing `Text` elements
|
||||
- Track selection state in a `Map<number, boolean>` or `boolean[]`
|
||||
- Use `renderer.addInputHandler()` for global keybinds (q, a, n, s, enter, space, arrows)
|
||||
- Launch via `Bun.$\`osascript -e '...'\`` or `Bun.spawn()`
|
||||
- Parse history.jsonl with `Bun.file().text()` + line-by-line JSON.parse
|
||||
- Git data via `Bun.spawn(["git", "-C", path, ...])`
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use Bun exclusively (no node, npm, npx)
|
||||
- Bun auto-loads .env — no dotenv
|
||||
- Prefer `Bun.file()` over `node:fs`
|
||||
- Use `Bun.$` for shell commands, `Bun.spawn()` for processes
|
||||
- No JSDoc comments, no excessive documentation
|
||||
- camelCase for variables/functions, PascalCase for types/classes
|
||||
- Keep it simple — this is a launcher, not a framework
|
||||
|
||||
Reference in New Issue
Block a user