Skip to content

Latest commit

 

History

History
181 lines (142 loc) · 14.1 KB

File metadata and controls

181 lines (142 loc) · 14.1 KB

Project Overview

Freshell is a self-hosted, browser-accessible terminal multiplexer and session organizer. It provides multi-tab terminal management with support for terminals and coding CLI's like Claude Code and Codex. Key features include session history browsing, AI-powered summaries (via Google Gemini), and remote access over LAN/VPN with token-based authentication.

Development Philosophy

  • We are working on an infinite schedule with infinite tokens. This is unusual! We do not have time pressure, and can do things correctly.
  • We use Red-Green-Refactor TDD for all changes but the most trivial (e.g. doc changes). We never skip the tests, and never skip the refactor.
  • We ensure both unit test & e2e coverage of everything.
  • Before starting anything, think - what's the most idiomatic solution for the technology we're using?
  • We prefer clean architecture and correctness over small patches.
  • We fix the system over the symptom.

Repo Rules

  • Always work in a worktree (in .worktrees)
  • Before creating a new worktree, ensure the repo-supported test suite is green on the intended base. If the suite is not green, pause before creating the worktree and notify the user with the failing command and failure summary.
  • New behavior changes start on a worktree branch from origin/main and are submitted as PRs targeting main.
  • Do not create or open a PR until the user explicitly approves PR creation for that branch/change. Preparing a branch, committing locally, and pushing the branch is fine; stop before gh pr create or any equivalent PR creation step unless approval is explicit.
  • Use dan@danshapiro.com when using gh.
  • Everything goes through a PR — never push behavior changes directly to origin/main.
  • Merge PRs once their required checks pass, then bring origin/main down to local main. Self-merging your own PRs is the norm. The only exception is a PR the user has said needs someone else to approve it first — leave those unmerged.
  • Many agents may be working in the worktree at the same time. If you see activity from other agents (for example test runs or file changes), respect it.
  • Specific user instructions override ALL other instructions, including the above, and including superpowers or skills
  • Server uses NodeNext/ESM; relative imports must include .js extensions
  • Always consider checking logs for debugging; server logs (including client console logs) are in the server process stdout/stderr (e.g., npm run dev/npm start).
  • Debug logging toggle (UI Settings → Debugging → Debug logging) enables debug-level logs and perf logging; keep OFF outside perf investigations.
  • When adding new user-facing features or making significant UI changes, update docs/index.html to reflect them. It's a nonfunctional mock of the default experience, so only major changes need to be added.

Test Coordination

  • Broad repo-supported test runs wait for the shared coordinator gate; if another agent holds it, wait rather than kill a foreign holder.
  • Set FRESHELL_TEST_SUMMARY when you want holder/status output to show a human-meaningful reason for a broad run.
  • Use npm run test:status to inspect the current holder, recent results, and any advisory reusable baseline.
  • Use npm run test:vitest -- ... for a repo-owned direct Vitest path. Raw npx vitest is not a coordinated workflow.
  • test:unit is the exact default-config test/unit workload, test:integration is the exact server-config test/server workload, and test:server stays watch-capable unless you pass an explicit broad --run.

Kata

  • .kata.toml is committed project configuration. Always commit it after modifying it.

Branch Model And Self-Hosting (CRITICAL - Read This)

Freshell no longer uses a local dev integration branch.

  • main is the only integration branch. Local main should track the merged state of origin/main.
  • Author changes in dedicated .worktrees/<slug> worktrees on feature branches created from origin/main.
  • Do not commit behavior changes directly to local main or push them directly to origin/main.
  • When a change is ready, push the feature branch, ask the user for explicit approval to create the PR, open a PR targeting main only after that approval, wait for required checks, merge the PR, then update local main from origin/main.
  • Update local main with a fast-forward pull or merge only. If local main has local-only commits, a dirty worktree, or cannot fast-forward, stop and resolve that explicitly instead of creating a local merge commit.
  • Delete or retire obsolete local dev worktrees/branches after confirming they are not running the self-hosted Freshell process.

Process Safety (CRITICAL)

  • Never use broad kill patterns (for example pkill -f "tsx watch server/index.ts", pkill -f vite, pkill node).
  • Start manual worktree servers on a unique port and record their PID, then stop only that PID.
  • Dev mode example (source + Vite): PORT=3344 npm run dev:server > /tmp/freshell-3344.log 2>&1 & echo $! > /tmp/freshell-3344.pid
  • Production mode example (built dist): PORT=3344 npm start > /tmp/freshell-3344.log 2>&1 & echo $! > /tmp/freshell-3344.pid
    • NEVER run node dist/server/index.js directly — use npm start which sets NODE_ENV=production; without it the server prints the Vite port (5173) in the startup URL even though Vite isn't running
  • Example stop: kill "$(cat /tmp/freshell-3344.pid)" && rm -f /tmp/freshell-3344.pid
  • Before stopping any process, verify it belongs to the worktree (ps -fp <pid> and confirm cwd/path includes .worktrees/...).
  • The self-hosted Freshell server must never be restarted without explicit user approval (the word "APPROVED"). Building is fine; deploying (stop + start) is not. The user's current Freshell session depends on it, and an unapproved restart will disconnect them mid-operation.

Codex Agent in CMD Instructions (Codex agents only; only when running in CMD on windows; all other agents must ignore)

  • Prefer bash/WSL over PowerShell; Windows paths map like D:\... -> /mnt/d/....
  • Use bash -lc "<cmd>" for non-interactive commands; avoid interactive shells so commands return control.
  • Apply_patch expects Windows-style paths.
  • If a bash command produces no visible output, rerun with tty: true to force output.
  • PowerShell may hang for dozens of seconds before starting in this tool; stick to bash unless explicitly required.
  • Don't make silly mistakes like installing Linux binaries in node_modules when we're on windows

Commands

Development

npm run dev                 # Run client + server concurrently with hot reload
npm run dev:client          # Vite dev server only (port 5173)
npm run dev:server          # Node with tsx watch for server auto-reload

Building

npm run build               # Full build (client + server)
npm run build:client        # Vite build → dist/client
npm run build:server        # TypeScript compile → dist/server
npm run serve               # Build and run production server
# `npm run serve` prompts before serving from a non-main branch; use
# `FRESHELL_ALLOW_NON_MAIN_SERVE=1 npm run serve` only when intentional.
# Note: `npm run build` is guarded — it will refuse to overwrite dist/
# if a production server is detected on the configured PORT. Use
# `npm run check` for safe verification, or build from a worktree.

Windows desktop builds (npm run electron:build:win) must run on native Windows — see docs/development/windows-electron-build.md.

Testing

npm test                    # Coordinated full suite (default + server configs)
npm run check               # Typecheck, then coordinated full suite
npm run verify              # Build, then coordinated full suite
npm run test:coverage       # Coordinated default-config coverage run
npm run test:status         # Show active holder, latest results, and advisory baseline info
npm run test:vitest -- ...  # Repo-owned direct Vitest path for focused passthrough work

External provider contract tests (test/integration/real/) spawn real claude, codex, and opencode binaries to verify external provider behavior, not Freshell code. They are opt-in and skipped by default to avoid blocking the coordinated suite on environment-dependent flakiness:

FRESHELL_RUN_REAL_PROVIDER_CONTRACTS=1 npm run test:vitest -- \
  run test/integration/real/ --config vitest.server.config.ts

Architecture

Tech Stack

  • Frontend: React 18, Redux Toolkit, Vite, Tailwind CSS, shadcn/ui, xterm.js, Zod
  • Backend: Node.js/Express, node-pty, WebSocket (ws), Chokidar, Vercel AI SDK + Google Generative AI
  • Testing: Vitest, Testing Library, supertest, superwstest

Directory Structure

  • src/ - React frontend application
    • components/ - UI components (TabBar, Sidebar, TerminalView, HistoryView, etc.)
    • store/ - Redux slices (tabs, connection, sessions, settings, claude)
    • lib/ - Utilities (api.ts, claude-types.ts)
  • server/ - Node.js/Express backend
    • index.ts - HTTP/REST routes and server entry
    • ws-handler.ts - WebSocket protocol handler
    • terminal-registry.ts - PTY lifecycle management
    • claude-session.ts - Claude session discovery & indexing
    • claude-indexer.ts - File watcher for ~/.claude directory
  • test/ - Test suites organized by unit/integration and client/server

Key Architectural Patterns

WebSocket Protocol: Schema-validated messages using Zod. Handshake flow: client sends hello with token → server validates → sends ready. Message types include terminal.create/input/resize/detach/attach and broadcasts like sessions.updated.

PTY Lifecycle: Each terminal has a unique ID. Server maintains 64KB scrollback buffer. On attach, client receives buffer snapshot then streams new output. On detach, process continues running (background session). Configurable idle timeout (15 mins default).

Claude Session Discovery: Watches ~/.claude/projects/*/sessions/*.jsonl for new files. Parses JSONL streams to extract messages, groups by project path.

Redux State Management: Slices for tabs, panes, connection, sessions, settings, claude. Persist middleware saves tabs and panes to localStorage. Async thunks for API calls.

Configuration Persistence: User config stored at ~/.freshell/config.json. Atomic writes with temp file + rename. Settings changes POST to /api/settings and broadcast via WebSocket.

Pane System: Tabs contain pane layouts (tree structure of splits). Each pane owns its terminal lifecycle via createRequestId and terminalId. When splitting panes, each new pane gets its own createRequestId, ensuring independent backend terminals. Pane content types: terminal (with mode, shell, status) and browser (with URL, devtools state).

Agent Status Indicators: Blue/busy status is derived from provider activity slices through resolvePaneActivity; green/needs-attention and the idle sound flow through recordTurnComplete and useTurnCompletionNotifications. Turn-complete (green/sound) is server-authoritative everywhere: terminal CLIs via terminal.turn.complete, and fresh-agent panes (freshclaude/kilroy/freshcodex/freshopencode) via a discrete freshAgent.turn.complete edge emitted only on a positive completion — freshclaude/kilroy on the SDK result with subtype === 'success', freshopencode on the success-only emitStatus(idle) path, and freshcodex on turn/completed only when params.turn.status === 'completed' (the notification also fires on interrupt). The client folds it in via applyFreshAgentCompletion using the at-monotonic dedupe regime (wall-clock at, no per-session counter, so a resumed durable session can't swallow completions across a server restart). The fragile client-side busy→idle derivation was removed; useAgentSessionTurnCompletion now only handles the waiting-for-approval edge. freshopencode still runs on a shared long-lived opencode serve sidecar and uses server-pushed session.idle/session.status events to drive busy. Gemini and Kimi terminal modes are status-inert until their CLIs expose a reliable turn-complete signal.

Fresh-Agent Orchestration: The REST agent API (/api/tabs, /api/panes/:id/split, /api/panes/:id/send-keys, /api/panes/:id/capture, /api/panes/:id/wait-for) and the MCP freshell tool accept agent/model/effort parameters to create and drive fresh-agent panes (e.g. agent=opencode). The orchestration layer dispatches to the registered FreshAgentRuntimeManager, so the same external surface works for any fresh-agent provider.

Data Flow

  1. Browser loads → fetches settings from /api/settings and sessions from /api/sessions
  2. WebSocket connects → client sends hello with auth token → server sends ready
  3. Terminal creation → Pane content has createRequestId → UI sends terminal.create WS message with that ID → server spawns PTY → sends back terminal.created with terminalId → pane content updated
  4. Terminal I/O → terminal.input WS messages write to PTY stdin → stdout/stderr streams to attached clients

Accessibility (A11y) Requirements

All components must be accessible for browser-use automation and WCAG compliance:

Semantic HTML:

  • Use <button>, <a>, <input>, <label> for interactive elements (not div with onClick)
  • Use semantic headers (<h1>-<h6>), nav, main, aside
  • Use proper form structure with labels associated to controls

ARIA & Labels:

  • Icon-only buttons: aria-label="Description" or <span className="sr-only">
  • Clickable cards/tiles: role="button" + aria-label
  • Custom components: appropriate roles and ARIA props
  • Complex widgets: aria-expanded, aria-pressed, aria-selected where applicable

Browser-use Requirements:

  • All interactive elements must be indexable (semantic HTML or proper roles)
  • All interactive elements must be identifiable (visible text or aria-label)
  • Never rely on selectors for automation; fix accessibility instead

Linting:

  • Run npm run lint to check a11y violations (eslint-plugin-jsx-a11y)
  • Fix with npm run lint:fix for auto-fixable issues
  • A11y linting is CI requirement before merging

Path Aliases

  • @/src/
  • @test/test/