diff --git a/agents/card_style.md b/.agents/card_style.md similarity index 100% rename from agents/card_style.md rename to .agents/card_style.md diff --git a/agents/frontend_controls.md b/.agents/frontend_controls.md similarity index 100% rename from agents/frontend_controls.md rename to .agents/frontend_controls.md diff --git a/agents/frontend_design_guide.md b/.agents/frontend_design_guide.md similarity index 100% rename from agents/frontend_design_guide.md rename to .agents/frontend_design_guide.md diff --git a/agents/python_test_guide.md b/.agents/python_test_guide.md similarity index 100% rename from agents/python_test_guide.md rename to .agents/python_test_guide.md diff --git a/agents/tables_style.md b/.agents/tables_style.md similarity index 100% rename from agents/tables_style.md rename to .agents/tables_style.md diff --git a/.config/wk.yml b/.config/wk.yml new file mode 100644 index 000000000..78d5461fa --- /dev/null +++ b/.config/wk.yml @@ -0,0 +1,2 @@ +open_workspace_cmd: "./.config/wt/start.sh" +restart_workspace_cmd: "./.config/wt/start.sh --restart" diff --git a/.config/wt.toml b/.config/wt.toml new file mode 100644 index 000000000..1eb3e8411 --- /dev/null +++ b/.config/wt.toml @@ -0,0 +1,5 @@ +[post-create] +deps = "uv sync && cd app/web_ui && npm install" + +[pre-remove] +session = "zellij delete-session {{ branch | sanitize }} 2>/dev/null || true" diff --git a/.config/wt/README.md b/.config/wt/README.md new file mode 100644 index 000000000..6c9968b9a --- /dev/null +++ b/.config/wt/README.md @@ -0,0 +1,169 @@ +# Worktree Workflow + +We use [Worktrunk](https://worktrunk.dev/) to manage git worktrees for parallel development. Each worktree gets its own Zellij session with dedicated ports, a backend server, a frontend dev server, and a coding agent. + +## Setup + +Run `utils/setup_env.sh` — it installs project dependencies and optionally sets up workspaces (worktrunk, Zellij, and config). + +Or manually: + +1. Install worktrunk: `brew install worktrunk && wt config shell install` +2. Install Zellij: `brew install zellij` +3. Install worktree TUI: `uv tool install "git+https://github.com/scosman/worktree_tui"` +4. Configure worktree path (one-time, applies to all repos): + +```bash +mkdir -p ~/.config/worktrunk +echo 'worktree-path = ".worktrees/{{ branch | sanitize }}"' > ~/.config/worktrunk/config.toml +``` + +## Command + +Just run `wk` and you can manage workspaces! + +## Advanced Commands + +### Create a new worktree and launch it + +```bash +wt switch --create my-feature -x .config/wt/start.sh +``` + +This creates a branch, sets up the worktree, installs dependencies (via `post-create` hook), then launches a Zellij session with all dev tools. + +To branch from something other than `main`: + +```bash +wt switch --create my-feature --base other-branch -x .config/wt/start.sh +``` + +### Launch an existing worktree + +```bash +wt switch my-feature -x .config/wt/start.sh +``` + +If the worktree already exists, this switches to it and launches Zellij. If the worktree was removed but the branch exists, it re-creates the worktree first. + +### Switch to a worktree (cd only, no Zellij) + +```bash +wt switch my-feature +``` + +### List all worktrees + +```bash +wt list +``` + +With CI status and line diffs: + +```bash +wt list --full +``` + +Include branches that don't have worktrees: + +```bash +wt list --branches +``` + +### Interactive picker + +```bash +wt switch +``` + +Running `wt switch` with no arguments opens an interactive picker with live diff/log previews. + +### Remove a worktree + +From inside the worktree: + +```bash +wt remove +``` + +From anywhere: + +```bash +wt remove my-feature +``` + +Force-remove with unmerged commits: + +```bash +wt remove -D my-feature +``` + +### Merge a feature branch + +From inside the feature worktree — squashes, rebases, fast-forward merges to main, and cleans up: + +```bash +wt merge +``` + +Merge to a different target: + +```bash +wt merge develop +``` + +Keep the worktree after merging: + +```bash +wt merge --no-remove +``` + +### Quick navigation + +```bash +wt switch - # Previous worktree (like cd -) +wt switch ^ # Default branch (main) +wt switch pr:123 # GitHub PR #123 +``` + +### View full diff of a branch + +All changes since branching (committed + uncommitted): + +```bash +wt step diff +``` + +### Commit with LLM-generated message + +```bash +wt step commit +``` + +### Cleanup merged branches + +```bash +wt step prune +``` + +## Keybinds for Option Left/right to switch tabs + +In `~/.config/zellij/config.kdl` add: + +``` +keybinds clear-defaults=true { + shared_except "locked" { + bind "Alt b" { GoToPreviousTab; } + bind "Alt f" { GoToNextTab; } + } +} +``` + +## Architecture + +- `.config/wt.toml` — project hooks: dependency install on create, Zellij session cleanup on remove +- `.config/wt/start.sh` — launches a Zellij session with per-branch ports (via `hash_port`) +- `.config/wt/layout.kdl` — Zellij layout: terminal, coding agent, backend server, frontend dev server +- `.config/wt/bin/web` — opens the worktree's web UI in a browser (type `web` in the terminal tab) +- `.config/wt/user_settings.sh` — per-user overrides (gitignored); copy from `user_settings.sh.example` +- `.config/wt/config.toml` — worktrunk user config (worktree path template) diff --git a/.config/wt/bin/web b/.config/wt/bin/web new file mode 100755 index 000000000..b90c74c12 --- /dev/null +++ b/.config/wt/bin/web @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +URL="${KILN_WEB_URL:-http://localhost:8758}" +if [[ "$(uname)" == "Darwin" ]]; then + open "$URL" +else + xdg-open "$URL" +fi diff --git a/.config/wt/config.toml b/.config/wt/config.toml new file mode 100644 index 000000000..3ce2ba2b2 --- /dev/null +++ b/.config/wt/config.toml @@ -0,0 +1 @@ +worktree-path = ".worktrees/{{ branch | sanitize }}" diff --git a/.config/wt/layout.kdl b/.config/wt/layout.kdl new file mode 100644 index 000000000..e3be3dde0 --- /dev/null +++ b/.config/wt/layout.kdl @@ -0,0 +1,31 @@ +layout { + default_tab_template { + pane size=1 borderless=true { + plugin location="zellij:tab-bar" + } + children + pane size=2 borderless=true { + plugin location="zellij:status-bar" + } + } + tab name="term" focus=true { + pane name="terminal" command="bash" start_suspended=false { + args "-c" "echo \"\nWeb UI: $KILN_WEB_URL\nBackend: http://localhost:$KILN_PORT\n\nType 'web' to open in browser.\n\"; exec $SHELL" + } + } + tab name="agent" { + pane command="bash" start_suspended=false { + args "-c" "$KILN_CODER_CMD" + } + } + tab name="backend" { + pane command="bash" start_suspended=false { + args "-c" "uv run python -m app.desktop.dev_server" + } + } + tab name="webui" cwd="app/web_ui" { + pane command="bash" start_suspended=false { + args "-c" "npm run dev -- --host --port $KILN_FRONTEND_PORT" + } + } +} diff --git a/.config/wt/start.sh b/.config/wt/start.sh new file mode 100755 index 000000000..39834e15a --- /dev/null +++ b/.config/wt/start.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +RESTART=false +if [ "${1:-}" = "--restart" ] || [ "${1:-}" = "-r" ]; then + RESTART=true + shift +fi + +BRANCH="${1:-$(git -C "$REPO_ROOT" branch --show-current 2>/dev/null || echo "main")}" +SESSION_NAME="${BRANCH//\//-}" + +hash_port() { + printf '%s' "$1" | cksum | awk '{print ($1 % 10000) + 10000}' +} +PORT=$(hash_port "$BRANCH") + +export KILN_PORT="$PORT" +export KILN_FRONTEND_PORT="$((PORT + 1))" +export VITE_API_PORT="$PORT" + +export KILN_WEB_URL="http://localhost:$KILN_FRONTEND_PORT" + +export KILN_CODER_CMD="claude" +if [ -f "$REPO_ROOT/.config/wt/user_settings.sh" ]; then + # shellcheck source=/dev/null + source "$REPO_ROOT/.config/wt/user_settings.sh" +fi + +export PATH="$REPO_ROOT/.config/wt/bin:$PATH" + +printf '\e]0;FEAT: %s\a' "$BRANCH" + +cd "$REPO_ROOT" + +LAYOUT="$REPO_ROOT/.config/wt/layout.kdl" + +SESSION_STATUS=$(zellij list-sessions --no-formatting 2>/dev/null \ + | awk -v name="$SESSION_NAME" '{ n=$1; gsub(/[[:space:]]/, "", n); if (n == name) { print (/EXITED/ ? "exited" : "active"); exit } }') || true + +if $RESTART; then + zellij kill-session "$SESSION_NAME" 2>/dev/null || true + zellij delete-session "$SESSION_NAME" 2>/dev/null || true + exec zellij -s "$SESSION_NAME" -n "$LAYOUT" +fi + +if [ -n "$SESSION_STATUS" ]; then + exec zellij attach "$SESSION_NAME" +else + exec zellij -s "$SESSION_NAME" -n "$LAYOUT" +fi diff --git a/.config/wt/user_settings.sh.example b/.config/wt/user_settings.sh.example new file mode 100644 index 000000000..1ef2bfce3 --- /dev/null +++ b/.config/wt/user_settings.sh.example @@ -0,0 +1,9 @@ +# Per-user settings for the Kiln dev environment. +# Copy this file to user_settings.sh and customize: +# cp user_settings.sh.example user_settings.sh +# +# user_settings.sh is gitignored — your changes stay local. + +# Coding agent command (default: claude) +# Examples: "claude", "cursor .", "aider", "zed ." +KILN_CODER_CMD="claude" diff --git a/.gitignore b/.gitignore index a4b8f5f71..cc44bed61 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ libs/server/build dist/ .mcp.json + +.config/wt/user_settings.sh +.worktrees/ diff --git a/app/desktop/dev_server.py b/app/desktop/dev_server.py index 88dc5047b..3ed7c7d4d 100644 --- a/app/desktop/dev_server.py +++ b/app/desktop/dev_server.py @@ -20,10 +20,12 @@ if __name__ == "__main__": setup_resource_limits() + port = int(os.environ.get("KILN_PORT", "8757")) + uvicorn.run( "app.desktop.dev_server:dev_app", host="127.0.0.1", - port=8757, + port=port, reload=True, # Debounce when changing many files (changing branch) reload_delay=0.1, diff --git a/app/web_ui/src/lib/api_client.ts b/app/web_ui/src/lib/api_client.ts index 8b4e9e0ed..a2cae4366 100644 --- a/app/web_ui/src/lib/api_client.ts +++ b/app/web_ui/src/lib/api_client.ts @@ -1,7 +1,8 @@ import createClient from "openapi-fetch" import type { paths } from "./api_schema" -export const base_url = "http://localhost:8757" +const api_port = import.meta.env.VITE_API_PORT || "8757" +export const base_url = `http://localhost:${api_port}` export const client = createClient({ baseUrl: base_url, diff --git a/hooks_mcp.yaml b/hooks_mcp.yaml index 8374e6997..093a3dc32 100644 --- a/hooks_mcp.yaml +++ b/hooks_mcp.yaml @@ -87,20 +87,20 @@ prompts: - name: "python_test_guide.md" description: "Guide for testing best practices for python code" - prompt-file: "agents/python_test_guide.md" + prompt-file: ".agents/python_test_guide.md" - name: "frontend_design_guide.md" description: "Guide for UI design including color, typography, and controls" - prompt-file: "agents/frontend_design_guide.md" + prompt-file: ".agents/frontend_design_guide.md" - name: "frontend_controls.md" description: "Guide for UI controls available in the web app (spinners, buttons, dialogs, etc)" - prompt-file: "agents/frontend_controls.md" + prompt-file: ".agents/frontend_controls.md" - name: "tables_style.md" description: "Guide for css table styling" - prompt-file: "agents/tables_style.md" + prompt-file: ".agents/tables_style.md" - name: "card_style.md" description: "Guide for css card styling" - prompt-file: "agents/card_style.md" + prompt-file: ".agents/card_style.md" diff --git a/libs/server/kiln_server/server.py b/libs/server/kiln_server/server.py index fbc1fb69b..a2c2a4bda 100644 --- a/libs/server/kiln_server/server.py +++ b/libs/server/kiln_server/server.py @@ -1,4 +1,5 @@ import argparse +import os from importlib.metadata import version from typing import Sequence @@ -45,11 +46,12 @@ def ping(): connect_document_api(app) connect_custom_errors(app) + frontend_port = os.environ.get("KILN_FRONTEND_PORT", "5173") allowed_origins = [ - "http://localhost:5173", - "http://127.0.0.1:5173", - "https://localhost:5173", - "https://127.0.0.1:5173", + f"http://localhost:{frontend_port}", + f"http://127.0.0.1:{frontend_port}", + f"https://localhost:{frontend_port}", + f"https://127.0.0.1:{frontend_port}", ] app.add_middleware( diff --git a/pyproject.toml b/pyproject.toml index 2c9a1b632..9667aad52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,10 +40,10 @@ kiln-studio-desktop = { workspace = true } kiln-ai = { workspace = true } [tool.ruff] -exclude = [] +exclude = [".worktrees"] [tool.ty.src] -exclude = ["**/test_*.py", "app/desktop/build/**", "app/web_ui/**", "**/.venv", "**/kiln_ai_server_client/**"] +exclude = [".worktrees", "**/test_*.py", "app/desktop/build/**", "app/web_ui/**", "**/.venv", "**/kiln_ai_server_client/**"] [tool.ty.rules] invalid-key = "ignore" diff --git a/utils/setup_env.sh b/utils/setup_env.sh index 7303c46e8..2add07d16 100755 --- a/utils/setup_env.sh +++ b/utils/setup_env.sh @@ -10,3 +10,48 @@ uv sync cd "$PROJECT_ROOT/app/web_ui" npm install +echo "" +read -rp "Install Kiln workspaces (worktree-based parallel dev with Zellij)? [y/N] " install_workspaces +if [[ "$install_workspaces" =~ ^[Yy]$ ]]; then + if ! command -v brew &>/dev/null; then + echo "Warning: Homebrew (brew) is not installed. Skipping workspace setup." + echo " Install it from https://brew.sh and re-run this script." + else + if ! command -v wt &>/dev/null; then + echo "Installing worktrunk..." + brew install worktrunk + wt config shell install + echo " Restart your shell (or open a new tab) for 'wt' completions to take effect." + else + echo " worktrunk already installed." + fi + + if ! command -v zellij &>/dev/null; then + echo "Installing zellij..." + brew install zellij + else + echo " zellij already installed." + fi + + if ! command -v wk &>/dev/null; then + echo "Installing worktree TUI..." + uv tool install "git+https://github.com/scosman/worktree_tui" + else + echo " worktree TUI already installed." + fi + + WT_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/worktrunk" + WT_CONFIG="$WT_CONFIG_DIR/config.toml" + WT_PROJECT_CONFIG="$PROJECT_ROOT/.config/wt/config.toml" + if [ ! -e "$WT_CONFIG" ] && [ -f "$WT_PROJECT_CONFIG" ]; then + mkdir -p "$WT_CONFIG_DIR" + ln -sf "$WT_PROJECT_CONFIG" "$WT_CONFIG" + echo " Linked worktrunk config: $WT_CONFIG -> $WT_PROJECT_CONFIG" + else + echo " Worktrunk config already present." + fi + + echo "Workspaces ready! See .config/wt/README.md for usage." + fi +fi +