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.
- 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.
- 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/mainand are submitted as PRs targetingmain. - 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 createor any equivalent PR creation step unless approval is explicit. - Use
dan@danshapiro.comwhen usinggh. - Everything goes through a PR — never push behavior changes directly to
origin/main. - Merge PRs once their required checks pass, then bring
origin/maindown to localmain. 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
.jsextensions - 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.htmlto reflect them. It's a nonfunctional mock of the default experience, so only major changes need to be added.
- 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_SUMMARYwhen you want holder/status output to show a human-meaningful reason for a broad run. - Use
npm run test:statusto inspect the current holder, recent results, and any advisory reusable baseline. - Use
npm run test:vitest -- ...for a repo-owned direct Vitest path. Rawnpx vitestis not a coordinated workflow. test:unitis the exact default-configtest/unitworkload,test:integrationis the exact server-configtest/serverworkload, andtest:serverstays watch-capable unless you pass an explicit broad--run.
.kata.tomlis committed project configuration. Always commit it after modifying it.
Freshell no longer uses a local dev integration branch.
mainis the only integration branch. Localmainshould track the merged state oforigin/main.- Author changes in dedicated
.worktrees/<slug>worktrees on feature branches created fromorigin/main. - Do not commit behavior changes directly to local
mainor push them directly toorigin/main. - When a change is ready, push the feature branch, ask the user for explicit approval to create the PR, open a PR targeting
mainonly after that approval, wait for required checks, merge the PR, then update localmainfromorigin/main. - Update local
mainwith a fast-forward pull or merge only. If localmainhas 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
devworktrees/branches after confirming they are not running the self-hosted Freshell process.
- 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.jsdirectly — usenpm startwhich setsNODE_ENV=production; without it the server prints the Vite port (5173) in the startup URL even though Vite isn't running
- NEVER run
- 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: trueto 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
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-reloadnpm 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.
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 workExternal 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- 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
src/- React frontend applicationcomponents/- 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 backendindex.ts- HTTP/REST routes and server entryws-handler.ts- WebSocket protocol handlerterminal-registry.ts- PTY lifecycle managementclaude-session.ts- Claude session discovery & indexingclaude-indexer.ts- File watcher for ~/.claude directory
test/- Test suites organized by unit/integration and client/server
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.
- Browser loads → fetches settings from
/api/settingsand sessions from/api/sessions - WebSocket connects → client sends
hellowith auth token → server sendsready - Terminal creation → Pane content has
createRequestId→ UI sendsterminal.createWS message with that ID → server spawns PTY → sends backterminal.createdwithterminalId→ pane content updated - Terminal I/O →
terminal.inputWS messages write to PTY stdin → stdout/stderr streams to attached clients
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-selectedwhere 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 lintto check a11y violations (eslint-plugin-jsx-a11y) - Fix with
npm run lint:fixfor auto-fixable issues - A11y linting is CI requirement before merging
@/→src/@test/→test/