H

August 26, 2026 · View on GitHub

AI-powered coding agent using DeepSeek.

← Back to README · 中文文档

Contents

Setup

# Install dependencies
npm run install:all

Configuration

H uses a client-entered API key model. Enter your DeepSeek API key once in the H UI under the model selector:

  1. Click the model selector in the agent console
  2. Enter your API key (starts with sk-...)
  3. Click Save

The key is sent once to the server and stored persistently on disk (~/.h/store/api-keys.enc, AES-256-GCM encrypted) — it is never persisted in browser localStorage, survives app restarts and updates, and is never re-sent in agent request bodies. The key remains stored until explicitly removed via "Remove API Key" in the UI or the ~/.h/ directory is deleted.

All client-side state (selected model, chat history, recent folder paths, open editor tabs, model presets, terminal history) is stored in browser localStorage and mirrored to ~/.h/store/client-state.json on every change and on app exit. This ensures data survives reinstalls, since %USERPROFILE%\.H\ is outside the Electron installer's scope. On startup, the client fetches GET /api/client/state and restores any previously saved state.

Get a key at platform.deepseek.com.

Storage Locations

DataLocationFormat
API Key~/.h/store/api-keys.encAES-256-GCM encrypted
Encryption key~/.h/.keyAuto-generated, machine-specific
Model & Thinking ModelocalStorage keys H-model, H-thinkingPlain text
Model PresetslocalStorage keys H-presets, H-active-presetJSON array
Chat History & PreferenceslocalStorage → mirrored to ~/.h/store/client-state.jsonJSON on disk
Agent Memory~/.h/memory/user_profile.md, ~/.h/memory/projects/<slug>/…Markdown + JSONL
File Tracking Metadata~/.h/store/file-tracking.jsonJSON (paths, sizes, checksums; no file contents)
Knowledge Graph Snapshots~/.h/snapshots/file-tree-snapshot-<hash>.kgEdge-list + import records (classification, unused)
Port Discovery%TEMP%/h-ports/express-port, vite-portRuntime only, not persisted
Single-instance Lock%TEMP%/H-pidPID file

Deleting ~/.h/ removes all H persistent data. Temp files are cleaned on clean shutdown.

Start

npm run dev

This starts both the backend and frontend. The OS assigns both ports; check console output for the URLs.

Port discovery flow:

  1. Express starts → server.listen(0) → OS assigns a free port → port written to ~/.h/ports/express-port
  2. Vite starts immediately (no blocking) → proxies /api, /ws, /_browser via middleware that reads ~/.h/ports/express-port on each request → returns 503 Service Unavailable until Express is live, then forwards normally
  3. Vite binds → port: 0 → OS assigns a free port → port written to ~/.h/ports/vite-port
  4. Electron (desktop mode) reads both files to connect to Express and load the Vite dev page

Both servers let the OS decide — no hardcoded port numbers anywhere.

Stale port cleanup: On shutdown (Ctrl+C, SIGTERM), Express deletes the port file. If Express crashes unexpectedly and the file lingers, the Vite proxy middleware detects the dead port (ECONNREFUSED), invalidates the cached port, and re-reads the file on the next request. Port files are stored in %TEMP%/h-ports/ (platform temp directory) to avoid filesystem permission issues on sandboxed environments.

Single-instance lock: The desktop app uses a custom PID-file lock instead of Electron's app.requestSingleInstanceLock() (which is unreliable on Windows sandboxed environments). On startup, it writes the current PID to %TEMP%/H-pid; if a PID file already exists with a live process, the new instance quits. On clean shutdown, the PID file is removed. This prevents port file trampling and shared-state (~/.h/ files) corruption that would occur if two Express servers competed for the same resources.

File integrity: All ~/.h/ files are local to the user's machine. If modified by external actors, the effects are non-destructive — the app detects corruption and resets gracefully:

FileIf tampered
ports/express-portwaitForOwnServerPort validates via /api/health + PID check. Wrong PID → timeout → app shows startup error.
ports/vite-portElectron loads wrong URL → connection refused → loading screen with timeout.
store/client-state.jsonJSON.parse failure → all state resets to defaults. Valid but wrong JSON → UI shows bad model/paths; model strings just fail API calls; paths are displayed, never auto-opened.
store/api-keys.encAES-256-GCM auth tag mismatch on decrypt → API keys reset. File is unreadable without the machine key at ~/.h/.key.
memory/ (markdown/JSONL)Corrupt/unreadable files are skipped on read; other memory files remain intact.
.keyReplaced or deleted → existing api-keys.enc becomes permanently unreadable (new key generated on next save).

Architecture

H is a client-server application with an optional Electron desktop shell.

┌─────────────────────────────────────────────────────────────┐
│  Electron Shell (desktop mode)                              │
│  ┌──────────────────────────┐  ┌──────────────────────────┐ │
│  │  Client (React + Vite)   │  │  Server (Express + WS)   │ │
│  │                          │  │                          │ │
│  │  Monaco Editor           │  │  Agent loop (tool-call)  │ │
│  │  xterm.js terminals      │  │  LSP stdio bridge        │ │
│  │  Agent console (SSE)     │  │  Terminal manager (PTY)  │ │
│  │  File tree / SCM panel   │  │  Browser reverse proxy   │ │
│  │  Browser webview         │  │  Git / FS / System APIs  │ │
│  └──────────┬───────────────┘  └────────────┬─────────────┘ │
│             │  HTTP + SSE + WebSocket       │               │
│             └───────────────────────────────┘               │
└─────────────────────────────────────────────────────────────┘

                    ┌──────▼──────┐
                    │  DeepSeek   │
                    │  API (HTTPS)│
                    └─────────────┘

Server (server/)

The Node.js Express server is the backbone. It owns all backend logic and never runs in the browser. The OS assigns a free port at startup, written to ~/.h/ports/express-port for discovery.

LayerFileRole
HTTP APIserver/index.tsREST endpoints for filesystem CRUD, git status/commit/diff, project detection, system stats, and agent chat (blocking + SSE streaming + step-by-step)
Agent loopserver/agent.tsTool-calling orchestration: receives user messages, sends tool definitions to DeepSeek, executes filesystem/terminal tools, manages browser tool handoff, compacts conversation history
DeepSeek bridgeserver/deepseek.tsRaw DeepSeek API calls — chat, tool-calling, and SSE streaming — with prefix-cache tracking plus API-backed usage and cache-token reporting
LSP bridgeserver/lsp.tsSpawns language servers over stdio, forwards diagnostics to the client, handles completions and hover
Terminal managerserver/terminalManager.tsCreates per-session shell processes (PTY via node-pty or pipe fallback), routes I/O between client WebSocket messages and child process stdio, auto-detects localhost URLs in terminal output
Browser proxyserver/index.ts (/_browser)Reverse-proxies external URLs through the server so the client iframe stays same-origin, strips X-Frame-Options headers, injects a restrictive CSP

3 transport channels to the client:

  • HTTP — Standard REST for file reads/writes, git operations, LSP diagnostics, agent chat init
  • SSE (Server-Sent Events) — One-way streaming for agent thinking/text/tool events during an agent turn
  • WebSocket — Bidirectional for terminal I/O (term:create, term:write, term:resize, term:kill) and server-to-client broadcasts (logs, errors, browser URL detection)

Client (client/)

The React + Vite frontend runs on an OS-assigned port (port: 0) in development. In desktop/production mode, the Express server serves the built static files directly from client/dist/.

PaneFileRole
EditorEditorPane.tsxMonaco Editor with tabs, file tree, SCM panel, built-in browser webview, and terminal tabs — the main workspace
Agent consoleAgentConsole.tsxChat interface for the AI agent. Sends user goals to /api/chat/agent/stream, consumes the SSE event stream, renders tool calls with spinners/results, prompts user for permission on run_in_terminal, and shows Accept/Reject diffs for file edits
Files panelFilesPanel.tsxFile explorer tree with create/rename/delete, right-click context menu, and folder expansion state persistence
TerminalTerminalPane.tsxxterm.js terminal tabs, connected via WebSocket to the server's terminal manager
SCM panelScmPanel.tsxGit staging area, commit history, diff viewer
Status barStatusBar.tsxLanguage selector, encoding, indentation, cursor position, go-to-line
Menu barMenuBar.tsxFile/Edit/View/Terminal/Help menus

The client never calls DeepSeek directly. All AI interaction flows through the server's agent loop, which owns the API key and tool execution.

Data flow (agent turn)

User types goal → AgentConsole
  → POST /api/chat/agent/stream (message, context, projectRoot)
     or POST /api/chat/agent/stream/stepbystep (IDE-driven mode)
  → Server builds dynamic system prompt (ITR), compacts history, calls DeepSeek
  → DeepSeek returns text/tool_calls via SSE stream
  → Server executes filesystem tools (read_file, write_file, etc.) directly
  → Interactive browser tools (click, type, etc.) yield SSE "browser_tool" event (sub-agent only)
  → AgentConsole sends browser command to EditorPane's webview
  → WebView executes the action, returns result
  → AgentConsole calls POST /api/chat/agent/stream/continue (toolCallId, result)
  → Loop continues until write_summary + task_complete
  → Successful write_summary is shown once in agent-body as markdown preview

Agent loop internals

The agent loop (agentLoopStream / agentLoop in server/agent.ts) orchestrates a conversation between the user, the model, and tools using a message array (state.messages). Each turn follows a fixed pattern:

┌──────────────────────────────────────────────────────────────┐
│  Agent loop iteration                                        │
│                                                              │
│  1. buildOpenAiMessages(state)                               │
│     ↓ Converts internal messages → DeepSeek API format       │
│     ↓ Pairs tool_calls with tool_call_id responses           │
│     ↓ Injects system prompt (ITR-selected chunks)            │
│                                                              │
│  2. chatDeepSeekToolStream(messages, tools)                  │
│     ↓ Sends to DeepSeek, receives SSE stream                 │
│     ↓ Yields thinking events, text, tool_calls               │
│                                                              │
│  3. For each tool call:                                      │
│     ├─ Push assistant tool_calls message to state.messages   │
│     ├─ Execute tool (filesystem / terminal / browser)       │
│     ├─ Push tool result message to state.messages            │
│     └─ Continue to next tool (batch) or next iteration       │
│                                                              │
│  4. If no tool calls → final text reply (only when no work was performed), otherwise must write_summary + task_complete │
│     → write_summary stores the final summary, AgentConsole renders it once in `agent-body`, then `task_complete` returns phase: "done" │
└──────────────────────────────────────────────────────────────┘

Message roles

The agent state tracks three message roles. Each serves a specific purpose in the conversation:

RoleWho creates itPurposeContent
userClient (createAgentSession)User's request / goalPlain text ("Add login endpoint")
assistant (text)DeepSeek API → server pushesModel's reasoning / repliesText response
assistant (with name)DeepSeek API → server pushesTool call requestJSON array of { id, function: { name, arguments } }
toolServer (after tool execution)Tool execution resultTool output (file contents, command output, browser result)

Message lifecycle

User sends request
  → state.messages = [{ role: "user", content: "..." }]

Iteration 1: DeepSeek decides to read a file
  → state.messages.push({ role: "assistant", name: "read_file", content: '[...]' })
  → Server executes read_file → returns file contents
  → state.messages.push({ role: "tool", content: "...", tool_call_id: "call_1" })

Iteration 2: DeepSeek reads the result, decides to edit
  → state.messages.push({ role: "assistant", name: "edit_file", content: '[...]' })
  → state.messages.push({ role: "tool", content: "Wrote ...", tool_call_id: "call_2" })

Iteration 3: DeepSeek is done
  → Calls write_summary → stores final structured summary
  → AgentConsole renders that summary once as an assistant message in `agent-body`
    using markdown preview (`###` headings, lists, inline code)
  → Calls task_complete → agent returns phase: "done" using the stored summary

Each tool call is always a matched pair: an assistant message with name containing the tool_calls JSON, followed immediately by a tool message with the same tool_call_id. buildOpenAiMessages() enforces this pairing — unpaired tool calls get synthetic error responses before the request reaches DeepSeek.

Desktop vs web mode

FeatureWeb (browser)Desktop (Electron)
Server processExternal (npm run dev:server on random or H_PORT port)Embedded child process: spawns same H.exe with ELECTRON_RUN_AS_NODE=1, runs pre-compiled CommonJS server in packaged builds, tsx in dev
ClientVite dev server (OS-assigned port) or Express static middlewareVite dev server (port 5173–5180) dev; packaged, Express serves the pre-built client/dist/ from inside app.asar
TerminalWebSocket to Express, pipe-fallback PTYWebSocket to Express, node-pty + ConPTY on Windows
File accessBrowser File System Access API or server FS APIsServer fs + native Electron dialog folder/file pickers
Built-in browseriframe proxied through Express (/_browser)Electron webview with geolocation, permissions, popup interception
Port allocationAny (server binds H_PORT or falls back to 51734–51753)Fixed range 51734–51753 for Express (written to stdout banner, parsed by main process), Vite dev server 5173–5180
Startup diagnosticTerminal stdout/stderrPersistent rolling log at ~/.h/logs/startup-<timestamp>-<pid>.log (survives installer console teardown)

Desktop (Electron)

H can also run as a desktop app (closer to VS Code) with an embedded server and a PTY-backed terminal. The desktop build ships two copies of the critical runtime assets to avoid Windows loader issues with archive paths:

  • app.asar archive (compressed, default): all pure-JS npm packages (express, ws, dotenv, typescript), Vite client production build (client/dist), server TS source, Electron scripts.
  • app.asar.unpacked/ directory (real NTFS paths, always at <install>/resources/app.asar.unpacked/):
    • Native modules that load via dlopen/LoadLibraryExW and cannot be read from inside an archive: node-pty/build/Release/*.node, @esbuild/win32-x64/esbuild.exe
    • The compiled Express server (dist/server/index.js, CJS output of tsc -p tsconfig.json)
    • The pre-require bootstrap (dist/server/bootstrap-packaged.cjs) — see below
    • A copy of client/dist and package.json (so resolveProjectRoot() in the compiled server can locate the project root without touching the asar archive virtual path)

Because the pure-JS packages (74% of installed node_modules weight) live in the compressed archive while only the ~3% that need real filesystem paths are unpacked, installer size stays near ~90–105 MB despite the duplicated dist/ folders.

Scripts

# Desktop dev (runs Vite + Electron)
npm run desktop:dev
# Build pipeline for desktop packaging (also runs via desktop:pack)
npm run desktop:build-server   # tsc -p tsconfig.json -> dist/server/index.js; copies bootstrap-packaged.cjs -> dist/server/
npm run desktop:build          # Vite client production build -> client/dist/

# Full Windows packaging (electron-builder)
npm run desktop:pack           # dist:clean -> icon:generate -> desktop:build-server -> desktop:build
                               #   -> electron-builder --win --dir (win-unpacked stage)
                               #   -> scripts/embed-icon.js -> electron-builder --win --prepackaged dist/win-unpacked (NSIS installer)

Notes:

  • npm install runs electron-rebuild for node-pty automatically (via postinstall).
  • The terminal prefers node-pty (ConPTY on Windows) and falls back to pipe mode if PTY isn't available.
  • dist:clean removes only installer artifacts (dist/win-*/, *.exe, *.blockmap, etc.). It never deletes dist/server/ (compiled server) or client/dist (Vite build), so repeated desktop:pack runs avoid re-compiling unchanged TypeScript/React sources.

Server startup (packaged build) in detail

  1. Spawnelectron/main.cjs starts an embedded server by re-spawning the same H.exe (process.execPath) with ELECTRON_RUN_AS_NODE=1 set in the child env. In AS_NODE mode Electron boots only the Node.js runtime (no Chromium, no BrowserWindow, no extra UI). Critical spawn options:
    • shell: false (Windows: avoids CMD tokenization bugs — project paths with spaces like D:\Work Projects\Harness\ would otherwise be split at spaces and mis-interpreted as two arguments)
    • windowsHide: true (prevents any visible conhost)
    • cwd: process.resourcesPath (<install>/resources, real directory — older builds wrongly passed app.getAppPath() as CWD which returns the path to app.asar (a FILE), which Windows CreateProcessW rejects with ENOENT)
    • stdio: [ignore, pipe, pipe] (main process parses the server stdout banner for the port)
  2. Bootstrap — argv is ["--require", "<unpacked>/dist/server/bootstrap-packaged.cjs", "<unpacked>/dist/server/index.js"]. Node runs --require modules synchronously before touching index.js. The bootstrap:
    • Uses require('module').Module.globalPaths.push() to register two CJS search roots: <resources>/app.asar.unpacked/node_modules (natives) and <resources>/app.asar/node_modules (all pure-JS packages via Electron's asar-aware require)
    • Calls Module._initPaths() to refresh Node's cached paths, also re-reading any inherited NODE_PATH env var
    • Without this step, line 1 of index.js (require("dotenv/config")) fails because the standard walk-up from app.asar.unpacked/dist/server/ never reaches sibling virtual path app.asar/node_modules/.
  3. Port discovery — Server listens on 127.0.0.1 using listenInRange(51734, 20): tries 51734 first, walks 51735…51753 on EADDRINUSE. On success it prints H server running on http://localhost:<port> to stdout. The Electron main process reads the piped stdout, regex-matches this banner (fast path), and/or falls back to a parallel TCP range scan of 51734–51753 + HTTP GET /api/health probe — health endpoint returns { pid, ok } so the scan correctly distinguishes the H server from any other program on the same port.
  4. Loader UI — While waiting, main shows a frameless splash window with a HTML data URL (doesn't need a server). On failure the splash updates itself with the error reason AND the full path to the startup log, plus the rolling 8 KB tail of the child's stdout/stderr rendered in a monospace scrollable panel.
  5. Diagnostic log — All spawn args, CWD, NODE_PATH, unpacked layout sanity checks, server stdout/stderr chunks, port parse event, scan result, child exit code, and any uncaught errors are written atomically to $HOME/.h/logs/startup-<YYYYMMDD>-<HHMMSS>-<parent pid>.log before any BrowserWindow is created. Because the file is on disk, it survives the NSIS installer's "Launch H" case where the installer kills its console the second the installer UI closes — the earlier builds lost all console output in that case.

Build output layout

<install>/
└── resources/
    ├── H.exe (main executable: process.execPath)
    ├── app.asar                       ← compressed archive: pure-JS node_modules, server TS source, client/dist, package.json, electron/
    └── app.asar.unpacked/             ← real NTFS directory, loaded by asar-aware loader + NODE_PATH preference
        ├── package.json               ← package.json sentinel (resolveProjectRoot() uses this for asar.unpacked branch)
        ├── dist/server/
        │   ├── bootstrap-packaged.cjs ← --require module that registers dual CJS search roots
        │   └── index.js               ← Express server (CommonJS, tsc output)
        ├── client/dist/               ← Vite production build (copy: resolveProjectRoot() + Express static middleware)
        └── node_modules/
            ├── @esbuild/win32-x64/    ← native esbuild.exe
            └── node-pty/              ← native bindings + package.json

Dev server startup differences

  • Server: dev spawn uses the same H.exe + AS_NODE trick, but passes node_modules/tsx/dist/cli.mjs server/index.ts — TS source is transpiled by tsx. findLiveServerPort still uses the stdout banner + 51734–51753 range scan.
  • Client: dev spawns a separate vite process (Vite picks a free port in the 5173–5180 range, writes it to a tiny Vite port file under $TMP/h-ports/ that's only used in dev, main process reads it). Vite dev server proxies /api, /ws, /_browser, /settings, /resources up to the fixed Express port range — proxy plugin in client/vite.config.ts uses the same TCP scan pattern with a 3-second cache, not any filesystem port handshake.

Built-in Browser (Desktop)

In Electron mode, H includes a full browser in the editor area powered by an Electron webview.

Features

Auto-detect localhost URLs — When a terminal process outputs a URL, H scans the output in real time and opens compatible URLs in a new browser tab automatically.

  • Detection pattern: Any URL matching http(s)://localhost, 127.0.0.1, 0.0.0.0, or [::1] with a port number (e.g. http://localhost:5173).
  • Web page vs API filtering: Before opening, H sends a quick HEAD request to check the Content-Type header. Only URLs returning text/html are opened as browser tabs — API endpoints (e.g. /api/health, JSON responses) are silently skipped.
  • Deduplication: Each URL is opened at most once per terminal session. Repeating the same URL in terminal output is ignored.
  • Supported sources: Works with both PTY and pipe-based terminals, scanning both stdout and stderr.

Manual navigation — Type a URL or a Bing search query in the address bar and press Enter or click Go.

Back / Forward / Refresh — Toolbar buttons with disabled state when navigation isn't available.

Site information — Click the security icon to see the connection status (secure/not secure), the current URL, and permission toggles.

Site permissions — Per-origin toggles for:

  • Geolocation — Uses Windows native location via PowerShell GeoCoordinateWatcher (no Google API key required). Location is cached IDE-wide and refreshed every 5 minutes. Works across all navigation without re-granting.
  • Camera / Microphone / MIDI / Autoplay

Tabbed browsing — Multiple browser tabs can be open at the same time, just like file tabs.

Title syncing — The browser tab label follows the page's <title>.

Pop-up interception — Links that would open a new Electron window are captured and opened as a new H browser tab instead.

Cross-navigation locationnavigator.geolocation is overridden at dom-ready so the page always uses H's native Windows location bridge, even after navigating between routes.

Note: Geolocation requires https:// or localhost. The Windows Location API must be enabled in Windows Settings (Privacy > Location).

Language Support (LSP)

H provides editor intelligence — continuous error/warning checking, completions, and hover — through two layers:

1. Built-in (no setup): Monaco validates these in the browser, live as you type:

  • JavaScript / TypeScript (JSX/TSX)
  • JSON, CSS / SCSS / LESS, HTML

2. Language Server (LSP): For everything else, H talks to a standard language server over stdio (server/lsp.ts). The architecture follows the VS Code model — push-based, real-time diagnostics via Server-Sent Events (SSE).

Architecture (VS Code-style push model)

┌─ Client (EditorPane.tsx) ─────────────────────────────────────┐
│                                                                │
│  User types in editor                                          │
│       │                                                        │
│       ▼ (250ms debounce)                                       │
│  POST /api/lsp/diagnostics  ─── fire-and-forget didChange     │
│       │                                                        │
│       │                              ┌──────────────────┐     │
│       │   GET /api/lsp/watch ─── SSE │  EventSource per │     │
│       │   (persistent connection)     │  language        │     │
│       │                              └──────┬───────────┘     │
│       │                                     │                  │
│       │    publishDiagnostics event ◄───────┘                 │
│       ▼                                                        │
│  monaco.editor.setModelMarkers() ── squiggles appear          │
└────────────────────────────────────────────────────────────────┘

┌─ Server (lsp.ts) ────────────▼────────────────────────────────┐
│                                                                │
│  notifyFileChange()                                            │
│       │                                                        │
│       ▼                                                        │
│  sendNotification("textDocument/didChange") ──► LSP process   │
│       │                                            │           │
│       │         textDocument/publishDiagnostics ◄──┘           │
│       ▼                                                        │
│  handleMessage() ── broadcasts to all SSE clients              │
│       │                                                        │
│       ▼                                                        │
│  client.write("data: {uri, markers}\n\n") ──► SSE stream      │
└────────────────────────────────────────────────────────────────┘

Key differences from polling-based approaches:

AspectOld (polling)New (VS Code-style)
Diagnostics deliveryClient polls /api/lsp/diagnostics every 250msLSP server pushes via SSE — instant
Cross-file analysisOnly the changed file was polledAll files receive diagnostics from any change
URI handlingPolling used module-level Map with manual key normalizationSSE streams normalized URIs directly to matching file
ConnectionOne HTTP request per file changeOne persistent SSE connection per language

How it works:

  1. SSE connection — When a file is opened, the client establishes a persistent GET /api/lsp/watch?rootPath=...&language=... SSE connection per language. The server holds the connection open and registers it in session.sseClients.

  2. File change notification — On content change (250ms debounce), the client fires a POST /api/lsp/diagnostics with the file text. The server sends textDocument/didOpen or textDocument/didChange to the LSP process and returns immediately (fire-and-forget).

  3. Diagnostics push — When the LSP server emits textDocument/publishDiagnostics, handleMessage() broadcasts the markers to ALL connected SSE clients for that language. The client receives the event, matches the URI to an open file, and calls monaco.editor.setModelMarkers() to render squiggles.

  4. Cross-file analysis — Because pyright (and other LSP servers) scan the entire workspace on any change, diagnostics for ALL files arrive via SSE and are applied simultaneously. Opening file2 immediately shows errors that pyright published during file1's analysis.

A language server is only used if its executable is found on your PATH. If it isn't installed, that language is simply skipped — no errors, no setup required.

URI normalization

Different LSP servers encode file URIs differently — pyright uses %3A for drive letters and %5C for backslashes, pylsp uses bare characters, some double-encode. The normalizeUri() function in server/lsp.ts handles all variants:

  • Progressive decodeURIComponent (handles double-encoding like %2520)
  • Per-character fallback for mixed raw/encoded URIs
  • Backslash → forward slash normalization
  • Case-insensitive matching (lowercase)

Supported languages and their servers

LanguageServer binaryInstall (example)
Pythonpyright-langserver (preferred)
pylsp (fallback)
npm i -g pyright
pip install python-lsp-server pyflakes
JavaScript / TS(Monaco built-in)
HTML / CSS / JSON(Monaco built-in)
Javajdtlsinstall Eclipse JDT Language Server
C#omnisharpinstall OmniSharp (-lsp)
C / C++clangdinstall LLVM/clangd
Gogoplsgo install golang.org/x/tools/gopls@latest
Rustrust-analyzerrustup component add rust-analyzer
Rubysolargraphgem install solargraph
PHPintelephensenpm i -g intelephense
Swiftsourcekit-lspships with the Swift toolchain
Kotlinkotlin-language-serverinstall kotlin-language-server
Markdownmarksmaninstall marksman
YAMLyaml-language-servernpm i -g yaml-language-server
SQLsqlsgo install github.com/lighttiger2505/sqls@latest

Additional servers are also mapped out of the box (Lua, Dockerfile, Vue, Svelte, Dart, Elixir, Haskell, Terraform, Clojure, OCaml, Zig, Scala, TOML, Bash) — install the corresponding binary and reload.

After installing a server, restart the backend (npm run dev:server, or npm run dev) so the new executable is detected.

Reducing false positives

H applies multiple layers of filtering to keep diagnostics high-signal:

Server-side diagnostic pipeline (lsp.tshandleMessage):

  • Severity filtering — Info (3) and Hint (4) diagnostics are dropped. Only Error and Warning reach the editor.
  • Validity checks — Diagnostics with negative line/column positions or inverted ranges (end before start) are discarded.
  • Per-file cap — Max 200 diagnostics per file to prevent UI flooding on legacy or untyped code.

Per-language LSP server tuning (applied via workspace/didChangeConfiguration at init):

Language / ServerTuning
JavaScript (Monaco built-in)Syntax-only validation; semantic/type checks disabled (no module graph in plain JS)
TypeScript (Monaco built-in)Full semantic checking with strict: false, noImplicitAny: false
Python / pyrighttypeCheckingMode: "basic", diagnosticMode: "openFilesOnly";
reportOptional* rules → "none" (idiomatic Python uses Optional without guards);
reportMissingImports"warning" (venv/monorepo resolution gaps);
reportAttributeAccessIssue, reportArgumentType, reportAssignmentType"warning"
Python / pyright (venv)Auto-detects .venv / venv / env / .env under the project root and passes venvPath + venv so pyright resolves site-packages
Python / pylspKeeps pyflakes (real bugs); disables pycodestyle, pydocstyle, mccabe, flake8, pylint
YAMLSchema-store lookups disabled — avoids "Schema not found" false positives when no matching schema exists
Go / goplsstaticcheck: false — disables opinionated style suggestions

Engine-level fixes:

  • mapSeverity defaults undefined severity to Error (LSP spec: omitted means error), not Warning.
  • Diagnostics cache is scoped per project root — SSE init flushes only the current session's URIs, not the global map.
  • Diagnostic entries are cleaned from the cache when the LSP session exits, preventing stale markers.

Adding a language

Add an entry to SERVER_SPECS in server/lsp.ts mapping the language id to its server binary, and (if needed) the file extension in detectLanguage in client/src/panes/fileModel.ts.

File Management

H includes a file explorer tree (FilesPanel) with full create, delete, and rename capabilities — both for your manual use and for the AI agent.

Creating files

  • Click the + button in the files header to create a new file. If no folder is selected in the tree, the file is created in the project root. If a folder is selected (click it once — it highlights), the new file is created inside that folder.
  • Folders are automatically created on demand when you add a file under a path that doesn't exist yet.

Right-click context menu

  • Right-click any item in the file tree to Rename or Delete it.
  • Deleting a folder removes it recursively.

AI Agent file access The AI agent has filesystem tools:

ToolDescription
read_fileReads a file with line numbers (or lists a directory)
write_fileCreates or overwrites a file with full content
edit_fileTargeted string replacement — send only the lines that change
list_filesLists directory contents (skips .git / node_modules)
search_filesRecursively find files/folders by name pattern
grepSearch file contents for a regex pattern
create_directoryCreates a new directory (and any parent dirs)
rename_fileRenames or moves a file or directory
delete_fileDeletes a file or directory (recursively)

All tools operate relative to the project root. The agent can browse, create, edit, and clean up files on its own — no manual intervention needed.

Server APIs

EndpointMethodDescription
/api/fs/create-filePOSTCreates a file (and parent dirs) if it doesn't already exist
/api/fs/deleteDELETEDeletes a file or directory (recursive for dirs)
/api/fs/renamePOSTRenames / moves a file or directory
/api/fs/read-binaryGETRead a file as binary (used by browser_upload_file)

Smart File Tracking

H uses a dynamic file tracking system that auto-detects Git availability — following the "workspace trust" pattern used by modern IDEs.

How It Works

┌─ On startup / folder open ───────────────────────────────────────┐
│                                                                    │
│  checkGitAvailable() → git --version                               │
│       │                                                            │
│       ├── Git exists ──► Git mode                                  │
│       │   Uses git status/diff for file change tracking            │
│       │   Full SCM support (branches, commits, push/pull)          │
│       │                                                            │
│       └── No Git ──────► Watcher mode                              │
│           Uses fs.watch (built-in Node API) to monitor changes     │
│           Metadata cache stored in ~/.h/store/file-tracking.json   │
│           No dependencies needed                                   │
│                                                                    │
│  → Status bar shows spinner + "Scanning..." during init            │
│  → File tree snapshot built eagerly (ready before first agent run) │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

┌─ Git installed mid-session ────────────────────────────────────────┐
│                                                                    │
│  Periodic check (every 30s) detects git --version returns success  │
│       │                                                            │
│       ▼                                                            │
│  Frontend shows dialog: "Git detected! Switch to Git tracking?"    │
│       │                                                            │
│       ├── Confirm → switchToGit()                                  │
│       │   • Stops file watcher                                     │
│       │   • git init (if no repo exists)                           │
│       │   • Compares cache state with filesystem                   │
│       │   • Auto-commits if needed                                 │
│       │   • Clears watcher cache                                   │
│       │                                                            │
│       └── Not Now → stay in watcher mode                           │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Tracking Modes

ModeDetectionFile ChangesSCM PanelStatus Bar
gitGit found on startup or switchgit status --porcelainFully functionalBranch icon + "main"
watcherNo Git availablefs.watch recursive + JSON cacheDisabledCrosshair icon + "Watcher"
loadingFolder just openedScanning filesystem + building snapshotSpinner + "Scanning..."
noneNo folder open

File Tree Context for AI Agent

When the agent runs, H sends the project file tree as part of the system prompt context. The snapshot is built eagerly on folder open (not deferred to the first agent call), so it's always ready.

┌─ Folder open ───────────────────────────────────────────────────────┐
│  → buildSnapshot() walks entire project (skips node_modules/.git)   │
│  → Knowledge graph built (~/.h/snapshots/file-tree-snapshot-<hash>.kg) │
│  → Visualization written to ~/.h/snapshots/file-tree-snapshot-<hash>.txt │
│  → Status bar: spinner + "Scanning..." during the walk              │
└────────────────────────────────────────────────────────────────────┘

┌─ First agent run ──────────────────────────────────────────────────┐
│  → "(no file tree changes since last update)" — snapshot matches    │
└────────────────────────────────────────────────────────────────────┘

┌─ Subsequent agent runs (same folder) ──────────────────────────────┐
│  → Only patches sent: "+ added files" / "- deleted files"          │
│  → Snapshot updated after each send                                │
└────────────────────────────────────────────────────────────────────┘

┌─ Large changes (>100 files differ) ────────────────────────────────┐
│  → e.g. after git checkout to a different branch                   │
│  → Falls back to sending a full tree instead of a massive patch    │
│  → Snapshot updated, subsequent calls return to normal patch mode  │
└────────────────────────────────────────────────────────────────────┘

┌─ Cross-session continuity ─────────────────────────────────────────┐
│  → Snapshot persists to disk                                       │
│  → Restarting IDE does not re-send full tree unless folder changed │
│  → New folder open → snapshot rebuilt → ready before first run     │
└────────────────────────────────────────────────────────────────────┘

Workspace Deduplication

Each workspace gets a unique snapshot filename keyed by an MD5 hash of its resolved absolute path. This prevents cross-project collisions and ensures the same folder always maps to the same graph file.

d:\Work Projects\H   → MD5 → a1b2c3d4e5f6
                             → ~/.h/snapshots/file-tree-snapshot-a1b2c3d4e5f6.kg
                             → ~/.h/snapshots/file-tree-snapshot-a1b2c3d4e5f6.txt

d:\Other Projects\app       → MD5 → f6e5d4c3b2a1
                             → ~/.h/snapshots/file-tree-snapshot-f6e5d4c3b2a1.kg
                             → ~/.h/snapshots/file-tree-snapshot-f6e5d4c3b2a1.txt
  • Same folder, same hash — reopening a project overwrites its existing snapshot (no stale duplicates).
  • Different folders, different hashes — each workspace has independent graph files.
  • Path changes break the link — renaming or moving the project folder produces a new hash and a fresh snapshot. The old file is orphaned (not auto-cleaned).
  • read_graph uses identical hashing — the tool locates the correct .kg file at query time by computing the same MD5 from the project root.

API Endpoints

EndpointMethodDescription
/api/file-tracking/statusGETCurrent tracking mode, Git availability, workspace path
/api/file-tracking/initPOSTInitialize tracking for a workspace ({ workspacePath })
/api/file-tracking/git-detectedGETPolled by frontend — returns { gitDetected: true } when Git appears mid-session
/api/file-tracking/switch-to-gitPOSTSwitch from watcher to git; auto-inits repo, compares state
/api/file-tracking/changesGETGet changed files (works in both modes)
/api/file-tracking/refreshPOSTForce re-scan workspace in watcher mode
/api/file-tracking/file-tree-contextGETFull tree (first call) or patch (subsequent calls) for agent system prompt
/api/file-tracking/reset-snapshotPOSTReset snapshot so next context call returns full tree

Files

FileRole
server/fileTracking.tsFileTrackingService — singleton orchestrating Git or watcher mode, periodic Git detection, snapshot/patch logic
server/fileTrackingStore.tsFileTrackingStore — lightweight JSON-backed cache (~/.h/store/file-tracking.json) for file metadata
server/knowledgeGraph.tsbuildKnowledgeGraph() — builds codebase graph (CONTAINS/EXPORTS/IMPORTS/IMPORTS_SYMBOL edges + named-import records with stdlib/local/third-party classification and unused-import detection), .kg serialization, .txt visualization, computeWorkspaceFingerprint()
~/.h/store/file-tracking.jsonOn-disk metadata cache for watcher mode
~/.h/snapshots/file-tree-snapshot-<hash>.kgPer-workspace Knowledge Graph — dir/file/symbol nodes, CONTAINS/EXPORTS/IMPORTS/IMPORTS_SYMBOL edges, per-file import records (classification + unused)
~/.h/snapshots/file-tree-snapshot-<hash>.txtHuman-readable visualization sidecar (nested tree with import annotations)

Knowledge Graph

H builds a codebase knowledge graph on folder open — a structured representation of every file, directory, exported symbol, and their relationships. This feeds the agent's system prompt as a compact nested tree and persists to disk as a .kg file for graph-based queries.

Schema

The graph has three node types, four edge types, and a per-file import record:

Node TypeFieldDescription
dirnameDirectory
filename, kind (extension)Source file, config, doc
symbolname, kindExported function, class, const, type, interface, enum, or default export
Edge TypeFrom → ToDescription
CONTAINSdir → file | dirStructural: parent directory contains child
EXPORTSfile → symbolA file exports a named symbol
IMPORTSfile → fileFile-level import (e.g. import './utils')
IMPORTS_SYMBOLfile → symbolPrecise symbol-level import (e.g. import { foo } from './utils')

Each named import also becomes a NamedImport record: { fromId, module, names, classification, targetFileId, targetNames, unused }. classification is local | stdlib | third-party (stdlib detection covers Python stdlib modules and Node.js built-ins), and unused lists imported names never referenced outside import statements — powering the unused_imports query.

Symbol Parsing

Symbols are parsed from TypeScript/JavaScript and Python source:

LanguageDetected from
TS/JS (.ts, .tsx, .mts, .cts)TypeScript compiler API AST: export function/class/const/type/interface/enum, export { x }, export default
Python (.py, .pyi, .pyx)Module-level (column 0) def foo(), class Foo:, and name = value / name: Type = value bindings

TS/JS export kinds detected by the AST:

KindDetected from
functionexport function foo()
classexport class Foo {}
constexport const x = ..., export { x }
typeexport type T = ...
interfaceexport interface I {}
enumexport enum E {}
defaultexport default function/class/expr

Named imports (import { foo, bar } from './module') are matched to target file exports to create precise IMPORTS_SYMBOL edges — so the graph knows exactly which symbol depends on which symbol, not just which files. Imports are parsed as named, default, namespace (import * as ns), require(), dynamic import(), and side-effect imports in JS/TS; import and from … import (with as aliases) in Python.

.kg Format (on disk)

A compact edge-list format in ~/.h/snapshots/file-tree-snapshot-<hash>.kg:

# Knowledge Graph v3 — D:\Work Projects\H
# Nodes: 384  Edges: 512  Imports: 120
# Format: n<id>|<type>|<parentId>||<name>|<kind>
#         e<id>|<fromId>|<toId>|<type>
#         i<fromId>|<module>|<classification>|<targetFileId>|<names>|<unused>
#   type: dir|file|symbol  classification: local|stdlib|third-party
#   names: imported:local pairs (comma-joined)  unused: local names (comma-joined)

n0|dir|||H|
n1|file|n0||README.md|md
n2|dir|n0||server|
n3|file|n2||index.ts|ts
n4|symbol|n3||app|const
n5|file|n2||fileTracking.ts|ts
n6|symbol|n5||getFileTrackingService|function
n7|symbol|n5||FileTrackingService|class

e0|n0|n1|CONTAINS
e47|n5|n6|EXPORTS
e72|n3|n6|IMPORTS_SYMBOL

in3|./fileTracking|local|n5|getFileTrackingService|
in3|./legacy|local|n9|unusedThing|unusedThing
in3|fs|stdlib||default:fs|
in3|express|third-party||default:express|

Each line is self-contained — parse with split("|"), reconstruct paths by walking parent chains. No JSON overhead, trivially diffable with line-based tools. The i lines carry per-file import records so queries like imports_of (with stdlib/third-party tags) and unused_imports can be answered directly from the snapshot without re-reading source files.

.txt Visualization (human-readable)

A nested tree with export/import annotations, written alongside the .kg file:

H {
  server {
    index.ts  (exports: app:const; → getFileTrackingService, FileTrackingService)
    fileTracking.ts  (exports: getFileTrackingService:function, FileTrackingService:class, ...)
    knowledgeGraph.ts  (exports: buildKnowledgeGraph:function, KnowledgeGraph:interface, ...)
  }
  client {
    src {
      App.tsx  (→ EditorPane)
      panes {
        EditorPane.tsx  (exports: EditorPaneHandle:interface; → FilesPanel, fileModel)
      }
    }
  }
}
# 124 exports
# 47 file-level imports
# 89 symbol-level imports

Filtering

The graph excludes secrets (.env), VCS internals (.git/), dependencies (node_modules/, vendor/), build output (dist/, .next/), IDE caches, binary/media files, and lock files. Project config dotfiles (.eslintrc.js, .prettierrc, .editorconfig) and dot-directories (.github/, .husky/, .storybook/, .vscode/) are included.

Groundwork for Graph-Based Reasoning

The knowledge graph is designed as input for graph machine learning and path prediction:

  • One-hop queries: "What file exports getFileTrackingService?" — follow EXPORTS backward.
  • Call graph traversal: IMPORTS_SYMBOL edges form a precise dependency graph — follow them to understand data flow.
  • PageRank: Files imported by many others have higher centrality — identifies core modules.
  • Markov chain path prediction: Transition probabilities over IMPORTS_SYMBOL edges answer "if you just edited symbol X, what file is most likely to need changes next?"
  • GNN input: Nodes carry features (type, kind, name) and edges carry (type). An adjacency matrix can be built directly from the .kg file for training graph neural networks on codebase structure.

Comparison with Graphify

H and Graphify share the same core idea: pre-build a knowledge graph so AI agents can answer structural questions with a single query instead of scanning raw files. The differences are in scope and design philosophy:

HGraphify
TriggerAlways-on, built-in IDE featureManually invoked CLI skill (/graphify)
AST parsingTypeScript compiler API (TS/JS) + Python (def/class/module bindings)Tree-sitter (23 languages)
LLM involvementZero — purely deterministicTwo-pass: deterministic AST + Claude subagents for semantic/concept extraction
Output formatCompact .kg edge list (token-optimized) + .txt visualization.graph.html (interactive), .graph.json (NetworkX), GRAPH_REPORT.md
MultimodalCode files onlyCode, PDFs, images, video, audio, diagrams
Community detectionNoneLeiden clustering — groups subsystems by edge density
Confidence taggingN/A (everything is EXTRACTED)EXTRACTED / INFERRED / AMBIGUOUS
Query interfaceread_graph tool — 6 query types (structure, exports, imports_of, exporters_of, dependents, unused_imports)Python NetworkX API + CLI
Update modelAuto-rebuilds on file watcher events (2s debounce)SHA256 cache — re-runs only changed files
Agent integrationSystem prompt rule + tool registryCLAUDE.md/AGENTS.md rules + PreToolUse hooks (fires before grep/glob)
FootprintLightweight, minimal token overhead — always readyHeavier but richer — HTML visualizations, plain-language reports, multi-format

H prioritizes zero-latency, always-on graph availability embedded in the IDE loop, with a purpose-built compact format for LLM token efficiency. Graphify prioritizes depth and breadth — multi-language, multi-format, semantic reasoning — trading setup time for richer architectural insight.

Security

H gives the AI agent access to your filesystem, terminal, and browser. The following mitigations protect against supply-chain risks (compromised API responses, model prompt injection, or malicious tool outputs).

API & Transport

  • All DeepSeek API calls use HTTPS (https://api.deepseek.com/v1).
  • Client-entered DeepSeek API keys are stored persistently on disk at ~/.h/store/api-keys.enc (AES-256-GCM encrypted), keyed by an HTTP-only session cookie. Keys are never written to browser localStorage.
  • Agent requests and /api/models no longer include the raw key in request bodies or query strings after the initial credential submission.
  • The API key is never exposed to child processes (see Terminal Sandbox below).
  • /api/chat/agent/config exposes only configuration status (apiKeyConfigured, source), not the key value itself.
  • /api/chat/agent/credentials is the only route that accepts a raw client-entered key, and it stores that key server-side with file-backed persistence (survives server restarts and app updates).

Tool-Level Guards

ToolGuardBlocks
browser_navigateURL validationjavascript:, data:, file: protocols. Only http:// and https:// allowed.
run_commandEnv sanitizationAny env var whose name contains KEY, SECRET, TOKEN, PASSWORD, CREDENTIAL, or starts with npm_ is stripped before the child process starts. Only PATH, HOME, USER, TEMP, SHELL, SYSTEMROOT, LANG are forwarded.
run_in_terminalUser permission + Command sanitizationUser must explicitly Allow each command before it runs. On Windows, bash syntax (2>&1, &&) is auto-corrected to PowerShell equivalents.
read_file / grep / list_files / write_file / edit_file / delete_file / rename_file / search_filesSecret-file blockAll filesystem tools refuse access to files matching .env, .env.*, credentials.*, secret.*, .pem, .key, .p12, .pfx, and config/*secret* / config/*key* paths. These files are also hidden from directory listings and search results.

Browser Sandbox

  • The iframe is sandboxed with allow-scripts allow-same-origin allow-forms allow-popups. Blocked: top-navigation (can't escape the frame), plugins, modals, pointer-lock, downloads.
  • A Content-Security-Policy header is injected into all proxied pages: default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self'; form-action *. This prevents proxied sites from making fetch() calls to H's own API endpoints.
  • The original site's X-Frame-Options and Content-Security-Policy headers are stripped to allow framing, but the injected CSP replaces them.

Human-in-the-Loop

TriggerMechanism
run_in_terminalAllow/Deny prompt in the agent console
write_file / delete_fileAccept/Reject undo cards in the agent console

Limitations

  • DeepSeek's API response is still trusted by design. If the model provider's infrastructure were compromised and injected malicious tool calls, the tool-level guards (URL validation, eval blocking, env sanitization) would catch the most dangerous classes of attack, but not all. Run H in isolated environments (VM, dev container) when working with untrusted projects.
  • run_command is not container-sandboxed. It uses child_process.spawn with a cleaned environment. For full isolation, run H inside Docker or a VM.
  • File writes are undoable via the UI, but the agent has full write access to the project directory.

Agent Tools (DeepSeek-powered)

The AI agent has access to these tools when working on your project:

Filesystem

ToolDescription
read_fileRead a file with line numbers — always read before editing
write_fileCreate or overwrite a file with full content (requires user accept/reject)
edit_fileTargeted edit by replacing old_string with new_string. Much cheaper — only send the lines that change. old_string must match exactly including whitespace/indentation. Use replace_all to replace all occurrences.
list_filesList files and directories in a given path
search_filesRecursively find files/folders by name pattern (case-insensitive)
grepSearch file contents for a regex pattern — find definitions, usages
create_directoryCreate a directory (and parents)
rename_fileRename or move a file or directory
delete_fileDelete a file or directory recursively

Terminal

ToolTypeDescription
run_commandSandboxRun a shell command with inline output. Fast, no permission needed. Use for: tests, lint, git, pip, npm, builds, grep. Output is summarized to key lines (errors, warnings, URLs); full output is cached for read_command_output.
run_in_terminalReal terminalRun a long-running command in a dedicated terminal tab. User must Allow each command. Use for: python app.py, npm start, flask, watch mode, interactive shells. The agent waits for the command to exit or produce recognizable output (traceback, server-started message, etc.) before receiving the result. Terminal output is captured in full for the UI tool card, and a summarized version (key error/success lines) is sent to the model to save tokens.
kill_terminalControlKill agent-spawned terminal sessions. kill_terminal kills all, kill_terminal index=N kills the Nth terminal (0-based, in order of creation). Only kills terminals the agent started — user-created terminals are untouched. Returns a message confirming which terminal was killed and its command. Use to stop servers, free ports, or clean up before finishing.

run_in_terminal lifecycle

Agent calls run_in_terminal
  → Command sanitized (Windows: strip 2>&1, && → ;)
  → User Allow/Deny prompt
  → Command runs in terminal tab
  → Agent waits for:
       - Process exit (onFinish)
       - Server started pattern (e.g. "listening on :3000")
       - Error detected (traceback, ModuleNotFoundError, npm ERR!, etc.) → 500ms flush delay
       - 120s timeout fallback
  → Full terminal output sent to UI tool card
  → Summarized output (key lines, 8 lines / 1200 chars max) pushed to model context
  → Full output cached in commandOutputStore for read_command_output with [cmd #N] key
  → Agent reads result and acts on errors or proceeds to browser

Windows/PowerShell compatibility: On Windows, the terminal runs PowerShell. Bash-isms that would fail silently are auto-corrected:

Bash syntaxProblemAuto-fix
2>&1PowerShell doesn't understand stderr redirect; causes parse errorStripped (PowerShell captures stderr natively)
&& (chain on success)PowerShell uses ; for command chainingConverted to ;

Output handling for long logs: If the terminal outputs thousands of lines (e.g. verbose app startup), only a summarized view reaches the model — error lines, warnings, and success markers from the full output, limited to 8 lines / 1200 characters. The full output is always available via read_command_output cmd_id=N with pagination (offset, limit) and regex filtering (filter).

Browser

ToolScopeDescription
Navigation
browser_navigateSub-agent onlyNavigate to a URL (http/https only). Creates a new browser tab if none exists, or navigates the active tab. Waits for the browser view to mount before returning (up to 2s).
browser_infoSub-agent onlyGet current browser tab state: URL, page title, load status, and open tab count.
Observation (read-only)
browser_screenshotSub-agent onlyGet a visual snapshot of the current page. The result is the actual page image (base64 PNG, capped at 1024×1024, with an overlaid coordinate grid and 100px axis labels so the vision model can read exact pixel positions) plus a short header — URL, title, and a Screenshot: WxH line that defines the coordinate space. The browser sub-agent runs on deepseek-v4-flash-vision-exp automatically (no user setup), so it sees the image natively and interacts by pointing at coordinates in the image. For other sub-agent types that lack vision (e.g. frontend-specialist), the flash vision model describes the screenshot and the text description is returned. There is no text grid — the image is the only output.
browser_consoleSub-agent onlyGet the last 50 console entries (log, warn, error, dialogs) to check for JS errors
browser_request_errorsSub-agent onlyGet failed network requests (4xx/5xx/CORS) to verify API calls and resource loads
Interaction (sub-agent only)
browser_clickSub-agent onlyClick at screenshot-image x,y coordinates (the header's Screenshot: WxH fixes the pixel space; the renderer scales to viewport coords). Aim at the element's center — the click snaps to the nearest interactive element and reports what was clicked (e.g. <button> "Sign In"). Dispatches full pointer/mouse event sequence.
browser_typeSub-agent onlyClick the input/textarea near screenshot-image x,y and type text into it — snaps to the nearest text field, clicks first, clears, then types with realistic keyboard events
browser_clearSub-agent onlyClear the value of the input/textarea at screenshot-image x,y
browser_selectSub-agent onlySelect an option from the native <select> near screenshot-image x,y by value or label
browser_scrollSub-agent onlyScroll the page by pixels or to top/bottom
browser_press_keySub-agent onlyPress a keyboard key (Enter, Escape, Tab, Arrows, etc.) on the active element
browser_waitSub-agent onlyWait for an element matching a CSS selector to appear (polls every 200ms, default 5s timeout)
Mouse / file upload
browser_move_mouseSub-agent onlyMove the cursor to screenshot-image x,y — triggers hover effects without clicking
browser_right_clickSub-agent onlyRight-click at screenshot-image x,y — dispatches contextmenu event
browser_upload_fileSub-agent onlySet files on a file input by absolute paths — pass the file input's x,y in the screenshot, or omit to use the page's first file input
Dialogs
browser_get_dialogSub-agent onlyCheck if the page is blocked on a JavaScript dialog (alert/confirm/prompt). Returns the pending dialog's id, type, and message.
browser_respond_dialogSub-agent onlyAnswer a pending dialog so the blocked page continues: accept=true/false for confirm, value for prompt text. The page auto-dismisses as Cancel after 2 minutes if unanswered.

Diagnostics

ToolDescription
read_problemsRead current IDE diagnostics from the LSP-based Problems tab — linter errors, TypeScript errors, warnings. Falls back to auto-detected build/lint command if no LSP diagnostics are cached. Call after making changes to verify no new errors.
read_graphQuery the codebase knowledge graph for structural/dependency information — what a file exports, who imports from a file, which files export a given symbol, the full directory tree. Much faster than grep for dependency questions.

read_graph — Knowledge Graph Queries

read_graph queries the codebase knowledge graph (see Knowledge Graph for schema details). It reads the .kg file from ~/.h/snapshots/ and answers structural questions without scanning file contents. Use it for dependency analysis, symbol discovery, and project structure exploration.

Query Types
QueryFormatDescriptionExample
StructurestructurePrint the full directory tree (dirs + files, no symbols)structure
Exportsexports <file>List all symbols exported by a file (TS/JS and Python)exports server/fileTracking.ts
Imports ofimports_of <file>List all symbols and files imported by a file; stdlib/third-party imports are taggedimports_of client/src/App.tsx
Exporters ofexporters_of <symbol>Find which files export a symbol with this nameexporters_of getFileTrackingService
Dependentsdependents <file>Find which files import from this file (reverse dependency)dependents server/fileTracking.ts
Unused importsunused_imports <file>List imports that are never referenced in the fileunused_imports server/agent.ts
Query Details

exports <file> — Returns every exported symbol with its kind:

server/fileTracking.ts exports:
FileTrackingService:class
getFileTrackingService:function
TrackingMode:type

imports_of <file> — Returns each imported name with its source. stdlib and third-party imports carry a tag:

server/agent.ts imports:
  chatDeepSeekTool from ./deepseek
  getSnapshotPath from ./hPaths
  fs from fs [stdlib]
  express from express [third-party]

exporters_of <symbol> — Case-insensitive symbol search. Useful when you know a function name but not its location:

Files exporting 'getFileTrackingService':
server/fileTracking.ts → getFileTrackingService:function
server/index.ts → getFileTrackingService:function

dependents <file> — Reverse dependency lookup. Shows which files import from a target, including indirect dependents via exported symbols:

server/fileTracking.ts is imported by:
client/src/App.tsx
client/src/panes/StatusBar.tsx
server/agent.ts
server/index.ts

unused_imports <file> — Lists imported names that never appear outside import statements. Useful for cleanup before a refactor:

server/legacy.ts unused imports:
  unusedThing (from ./legacy)
  oldHelper (from ./helpers)

structure — Full directory tree for orientation. Returns sorted paths — no nesting, just one path per line for token efficiency:

Directory tree (384 entries):
H
H/.eslintrc.js
H/client
H/client/index.html
H/client/package.json
...
When to Use read_graph vs read_file vs grep
QuestionUseWhy
"What does fileTracking.ts export?"read_graph exportsDirect lookup — no file scanning
"What is the content of fileTracking.ts?"read_fileContent, not structure
"Where is initFileTracking called?"grepContent search across files
"Who imports from fileTracking.ts?"read_graph dependentsReverse dependency — impossible with grep alone
"What files export a function named foo?"read_graph exporters_ofSymbol-level query — grep would match comments, strings, calls
"Which imports are unused in agent.ts?"read_graph unused_importsTracked at build time — grep can't distinguish declarations from usage
"Find all .ts files in server/"list_files or search_filesFile/directory listing
"What does this project look like?"read_graph structureFull tree in one call

Key principle: read_graph answers structural questions (what exists, how things connect). read_file and grep answer content questions (what's inside, where is it used). When unsure, prefer read_graph for dependency/export queries — it's a single call vs potentially dozens of grep searches.

Control

ToolDescription
write_todosCreate or update a structured task list to track progress. In step-by-step mode, this is the ONLY tool available during planning — the agent must create a complete plan before any execution begins.
write_summaryWrite the final structured summary using the template: ### Changes Made, ### Verification, ### Outcome. Vague summaries are rejected. If write_todos was used: the summary must also include a ### Todo Progress section listing each item's final status. On success, the UI renders the summary once in agent-body as markdown preview instead of as a tool card.
task_completeFinalize the run. Has no parameters and is rejected unless write_summary has been called. Also rejected if any todo items are still pending/in_progress. The SSE done reply reuses the stored summary, but the client dedupes it so the final summary is not rendered twice.
delegate_taskDelegate a sub-task to a specialized sub-agent (browser, code-search, code-writer, researcher, planner, frontend-specialist, backend-specialist, security-auditor, architect-analyst, docs-analyst, documentation-writer) that runs independently with its own context window. Sub-agents run sequentially — each must complete before the next starts.

Multi-Agent Delegation

H supports sub-agent delegation — the main agent can spawn specialized sub-agents to handle complex sub-tasks in isolation. Each sub-agent gets its own context window, so its conversation history does not bloat the parent agent's context. Sub-agents run sequentially — each must complete before the next starts, managing RAM usage.

Architecture

┌──────────────────────────────────────────────┐
│  Parent Agent (Orchestrator)                 │
│  - Breaks down user request with write_todos │
│  - Calls delegate_task for each sub-task     │
│  - PAUSES while sub-agent runs               │
│  - Resumes, synthesizes, writes summary      │
│  - Calls task_complete                       │
└──────┬───────────────────────────────────────┘
       │  delegate_task starts sub-agent
       │  Sub-agent tools stream LIVE to UI
       │  (each with sub-agent color coding)

┌──────────────────────────────────────────────┐
│  Sub-Agent (isolated AgentState + context)   │
│  ┌─ tool_start read_file ──► result         │
│  ├─ tool_start edit_file  ──► result        │
│  ├─ tool_start run_command ─► result        │
│  └─ final plain-text report to parent        │
│  Browser sub-agents pause for renderer       │
│  results and resume via /continue            │
└──────────────────────────────────────────────┘


Parent resumes ← result pushed to parent's state.messages

Agent Profiles

ProfileToolsIterationsDescription
browserbrowser_navigate, browser_info, browser_screenshot, browser_click, browser_type, browser_clear, browser_select, browser_press_key, browser_console, browser_request_errors, browser_scroll, browser_wait, browser_move_mouse, browser_right_click, browser_upload_file, browser_get_dialog, browser_respond_dialogUnlimitedFull browser automation — no turn limit, runs until the task is done. Navigates, screenshots (pixel image), clicks/types by coordinate on the image, scrolls, fills forms, checks console/network, and answers blocked alert/confirm/prompt dialogs.
code-searchread_file, list_files, search_files, grep, read_graphUnlimitedRead-only code exploration. Finds files, reads code, reports findings. Never edits.
code-writerFull filesystem + run_command, read_problems, read_graphUnlimitedImplements features or fixes bugs. Reads, edits, builds, and verifies.
researcherread_file, list_files, search_files, grep, run_command, read_graphUnlimitedExplores codebase to answer questions. Reports with file paths and line numbers.
plannerread_file, list_files, search_files, grep, read_graph, write_todosUnlimitedAnalyzes project and creates structured step-by-step plans. Outputs a todo list with ordered, actionable steps.
frontend-specialistFull filesystem + run_command, read_problems, read_graph, browser_screenshot, browser_console, browser_request_errorsUnlimitedImplements UI features and components. Visually verifies changes in the browser.
backend-specialistFull filesystem + run_command, read_problems, read_graphUnlimitedImplements API routes, services, and database logic. Focuses on server-side patterns and data integrity.
security-auditorread_file, list_files, search_files, grep, run_command, read_graph, read_problemsUnlimitedAudits code for vulnerabilities. Runs security scans, reports findings with severity and remediation. Never edits.
architect-analystread_file, list_files, search_files, grep, read_graphUnlimitedAnalyzes project architecture, dependency graphs, and module structure. Reports architectural concerns and recommendations. Never edits.
docs-analystread_file, list_files, search_files, grep, read_graphUnlimitedAudits documentation coverage and quality. Identifies gaps and outdated docs. Never edits.
documentation-writerread_file, write_file, edit_file, list_files, search_files, grep, read_graph, create_directoryUnlimitedCreates or improves documentation. Writes README sections, API docs, and guides.

Key Design

FeatureDetail
Context isolationEach sub-agent has its own AgentState — messages do not pollute the parent's context
Tool allowlistingSub-agents receive only the tools their profile specifies (e.g. code-search can never write files)
Headless executionAll non-browser sub-agents run entirely server-side — no browser or terminal tools. Frontend-specialist has read-only browser tools for visual verification.
Browser delegationThe parent agent has NO browser tools — not even read-only ones. ALL browser interaction (navigating, taking screenshots, checking console/network, clicking, typing, scrolling) goes through the browser sub-agent via delegate_task agent_type: "browser". This keeps the main agent's context clean and forces structured delegation.
Live streamingSub-agent tool calls stream live to the UI as colored tool cards in real-time. Parent appears paused during delegation. Sub-agent text events are filtered — only tool_start/tool_end cards are shown, preventing message pollution.
Result summarizationSub-agent results are compressed before returning to the parent, preserving context budget
ParallelismNot supported — sub-agents run sequentially. Each must complete before the next starts, managing RAM usage. The agent should call delegate_task multiple times for independent sub-tasks.
Color codingEvery tool card has a left-border color: blue (main), teal (browser), green (code-search), amber (code-writer), purple (researcher), indigo (planner), cyan (frontend-specialist), blue (backend-specialist), red (security-auditor), orange (architect-analyst), lime (docs-analyst), pink (documentation-writer). Makes it easy to identify which agent executed each tool call.
Agent footerShows "Completed" when the conversation finishes normally (SSE done event). Shows "Stopped" only on errors or a 5-minute safety timeout. The footer label corresponds strictly to SSE stream state — not app focus.
Background operationThe SSE stream uses fetch-based streaming — operates independently of window focus. Agent conversations continue in background with no interruption when the app is minimized or behind other windows.

Usage

The parent agent uses these tools just like any other:

Agent: write_todos todos=[
  {id:1 text:"Research existing auth code" status:pending},
  {id:2 text:"Add login endpoint" status:pending}
]

Agent: delegate_task task="Find all authentication-related code 
  in the project. Report file paths, line numbers, and patterns used."
  agent_type="code-search"

→ [Code Search Agent] Completed in 4 turns.
  Found auth code in:
  - server/auth.ts:45-120 (JWT verification, password hashing)
  - client/src/Login.tsx:1-80 (login form component)
  ...

Browser agent example — the sub-agent can now navigate, click, type, and inspect pages interactively:

Parent: delegate_task task="Go to http://localhost:3000/login,
  type 'admin' into the email field, type 'pass123' into the
  password field, click Sign In, and report what happens."
  agent_type="browser"

→ [Browser Agent] browser_navigate http://localhost:3000/login
  → Renderer executes → page loads
→ [Browser Agent] browser_screenshot
  → Renderer returns the page image + header
    (URL, title, "Screenshot: WxH" image size)
  → Agent sees the login form in the image
→ [Browser Agent] browser_click x=320 y=180  (email field it sees)
  → Renderer scales to viewport coords → input focused
→ [Browser Agent] browser_type x=320 y=180 text="admin"
  → Renderer types → "admin" entered
→ [Browser Agent] browser_click x=320 y=240  (password field)
→ [Browser Agent] browser_type x=320 y=240 text="pass123"
→ [Browser Agent] browser_click x=280 y=300  (Sign In button)
→ [Browser Agent] browser_screenshot
  → Renderer returns the page image + header
  → Sees "Welcome, admin!" rendered in the image

→ [Browser Agent] Completed in 10 turns.
  Login test: SUCCESS. Navigated to login page,
  filled email and password fields, clicked Sign In.
  Result: "Welcome, admin!" displayed.

Parallel example — code-writer + researcher run simultaneously:

Agent: delegate_parallel tasks=[
  {task:"Implement POST /api/login in server/auth.ts", agent_type:"code-writer"},
  {task:"Research how existing API routes handle error responses", agent_type:"researcher"}
]

→ 2 sub-agents completed.
  [1] Wrote ~500 tokens to server/auth.ts. Build passed.
  [2] Found error handling pattern in server/middleware.ts:30-55...

When to use

  • delegate_task: For complex sub-tasks that would take many turns (deep research, feature implementation, multi-file refactoring, browser testing). For multiple independent sub-tasks, call delegate_task sequentially — each completes before the next starts.

IDE-Driven Step-by-Step Execution (Force Todo)

In the default agent loop, the LLM controls when to create, update, and complete todos — it can skip steps, forget updates, or jump ahead. Step-by-step mode inverts this: the IDE/server locks the todo list and forces the agent through each step one at a time via isolated sub-agents.

How it works

User sends task → POST /api/chat/agent/stream/stepbystep

Phase 1: PLANNING
  Agent only has write_todos — no other tools
  Agent MUST create a complete, ordered plan
  → Server validates (non-empty, specific steps)

Phase 2: LOCKED
  Server locks the todo list → SSE "step_plan" event
  UI renders the locked plan in the pending banner

Phase 3: EXECUTE (per step)
  For each pending todo:
    → SSE "step_begin" event
    → Code-writer sub-agent spawned with ONLY this step's context
    → Sub-agent has full filesystem + run_command tools (no turn limit — runs until done)
    → Streaming tool_start/tool_end events shown in UI
    → Sub-agent returns a final plain-text report → SSE "step_end" event
    → Step marked completed/failed, next step begins
  Previous step results are passed as context to subsequent steps

Phase 4: WRAP-UP
  → SSE "done" event with allStepResults summary

Key differences from default mode

AspectDefault ModeStep-by-Step Mode
PlanningAgent can plan AND execute in same turnStrict separation: plan first, execute later
Todo ownershipAgent-driven — LLM chooses when to updateIDE-driven — server locks todos, forces progression
ExecutionAgent works on anything at any timeOne step at a time, isolated sub-agent per step
ContextFull conversation history in one loopEach step gets fresh sub-agent with only that step + previous results
Tool restrictionFull tool set availablePlanning: only write_todos. Execution: code-writer tools (no browser/terminal)
Turn limitsNone — runs until the task completesNone — planning and per-step sub-agents run until complete

Why use it

  • Deterministic execution: The server enforces todo progression — agent can't skip or forget steps
  • Isolated failures: If a step's sub-agent fails, subsequent steps still run with clear failure context
  • Clean context per step: Each step sub-agent starts fresh, avoiding context bloat from earlier steps
  • Verifiable progress: The UI shows a locked plan with per-step status (pending → in_progress → completed/failed)

Summary Lock

H enforces structured summaries on write_summary. The server validates every summary against a required template, and rejects task_complete unless write_summary has been called.

### Changes Made
- [file path]: [what was changed]
### Verification
- [build/test/check result]
### Outcome
- [concise description of what was accomplished]

Summaries that are too short, match thought-process patterns (e.g. "I did the task", "OK, completed"), or lack concrete details (no file references, actions, or results) are rejected. The agent receives the rejection as a tool error and must call write_summary again with a proper summary.

Problem Lock

H enforces problem checking before task completion. task_complete is rejected if read_problems has detected unresolved errors (errorCount > 0). The agent must fix all errors, call read_problems again to verify they are resolved, and only then may call task_complete.

Agent: edit_file — makes a change
Agent: task_complete
  → REJECTED: "Cannot complete: 3 errors still present."
Agent: read_problems
  → LSP diagnostics: 0 errors, 0 warnings.
Agent: task_complete
  → ACCEPTED

read_problems aggregates diagnostics from two sources:

  • LSP diagnostics (real-time, per-file) — TypeScript errors, linter warnings, etc. from the IDE's language server
  • Build command fallback — if LSP is unavailable, runs the project's build/lint command (npx tsc --noEmit, python -m compileall, etc.)

The problem lock ensures the agent cannot declare a task complete while known errors remain in the project. Warnings do not block — only errors (severity === 8 in LSP, or non-zero exit code from build commands).

Persistent Memory

H includes a cross-session memory system backed by plain files (~/.h/memory/). The agent can store key decisions, user preferences, project conventions, and discovered patterns — and recall them in future sessions. Stored memory is also auto-injected into the system prompt every turn, so the agent always knows the user without needing to call a tool first.

How it works

READ (every turn, automatic):
  user message arrives → /api/chat/agent[/stream]
    ├─ autoCapturePreference(message)     — writes explicit preferences (best-effort)
    └─ selectMemoryContext(root, message) — ranks entries (keyword + cached embeddings)
                                             → state.memorySelection (top-K)
  buildSystemPrompt(CORE_RULES + memory block + chunks)
    └─ memory block = memorySelection || getMemoryContext(root)   ← always present

WRITE (explicit):
  Agent calls remember(key, value, category, tags, scope)
    ├─ scope = explicit || guessScope(key, value) || "project"
    └─ key normalized (near-duplicates merge); overwrites logged to history.jsonl
  → written to user_profile.md (scope=user) or projects/<slug>/project_memory.md (scope=project)

MAINTENANCE (async, after each run):
  runMemoryMaintenance(root) — merges semantic duplicates (embedding cosine > 0.95)

Tools

ToolDescription
rememberStore a key decision, user preference, project convention, or important fact. Persists across sessions in files. scope selects the user profile (user) or project memory (project). Categories: decision, preference, convention, fact, pattern, general. Tags help group related memories.
recallSearch stored memories by keyword or exact key. Also greps topics.md and session_memory_*.jsonl for matching context. Pass no params to list all memories.
forgetRemove a stored memory by its key. Use when a decision is reversed, a preference changes, or stored information becomes outdated.

Write accuracy

  • Scope guard (guessScope): deterministic rules correct the model-chosen scope — identity/global facts (timezone, preferred-model, language, …) are forced to user; codebase-specific keys (api-auth-method, server-port, paths ending in .ts/.py/…) are forced to project. An explicit scope passed by the model wins over the guess.
  • Key normalization & dedup (normalizeKey): "Indent Style" and "indent-style" are the same memory — re-remembering with a spelling variant updates the existing entry instead of duplicating.
  • Overwrite history: when a value changes, the old value is appended to history.jsonl next to the memory file — nothing is silently lost.
  • Automatic capture (autoCapturePreference): high-precision regex patterns (I prefer …, from now on use …, let's use …) extract explicit preferences from the user message and store them as pref-* entries. Repeats are skipped; ordinary conversation is never captured.

Relevance-ranked injection

  • Each user turn, selectMemoryContext() scores every entry against the current message (keyword hits + cosine similarity to a cached per-entry embedding). The top-K (default 6) replace the fixed 4000-char dump in state.memorySelection; the prompt builder consumes it synchronously — zero network calls per agent iteration.
  • Entry embeddings are cached and invalidated only when the value changes. If embeddings are unavailable (API/tier), ranking falls back to keyword-only.
  • Sub-agents get a compact user-profile subset (getUserProfileContext(), ≤1500 chars) so delegated tasks also respect cross-project preferences.

Storage

DetailValue
User memory~/.h/memory/user_profile.md (cross-project)
Project memory~/.h/memory/projects/<slug>/project_memory.md (per project)
Overwrite/merge historyhistory.jsonl beside each memory file
Session log~/.h/memory/projects/<slug>/<YYYYMMDD>/session_memory_<sessionId>.jsonl (append-only)
Topics~/.h/memory/projects/<slug>/<YYYYMMDD>/topics.md (goal/progress/summary)
API KeysAES-256-GCM encrypted file at ~/.h/store/api-keys.enc (persistent, survives restarts and app updates)
Client StateJSON file at ~/.h/store/client-state.json — mirrors all browser localStorage data (model, chat history, recent paths, open tabs, presets, terminal history) so it survives reinstalls
RetrievalKeyword scoring + cached-embedding cosine ranking (generateEmbedding); keyword-only fallback

When the agent uses memory

  • Proactive storage: When the user says "let's use X", "I prefer Y", or establishes a project convention, the agent calls remember without being asked (autoCapturePreference may capture it first).
  • Every turn: user profile + project memory are injected into the system prompt automatically — no recall needed for stored facts.
  • Session startup: recall still helps surface older topics.md / session_memory_*.jsonl context not stored as structured entries.
  • Memory cleanup: When preferences change or decisions are reversed, the agent can forget outdated entries.

Files

FileRole
server/memory.tsMemoryStore (CRUD + keyword search + session/topic logs), guessScope, normalizeKey, autoCapturePreference, selectMemoryContext, runMemoryMaintenance, raw profile read/write
server/agent.tsrunMemoryTool(), scope guard in remember, memory block in buildSystemPrompt, sub-agent injection
server/index.tsAuto-capture + relevance selection before the loop, maintenance after the run, GET/POST /api/memory/profile
client/src/panes/SettingsDialog.tsxSettings → Memory tab (edit user_profile.md)

Client State Persistence

All h-prefixed localStorage keys are mirrored to ~/.h/store/client-state.json so UI state survives reinstalls. The agent console stores one key per project folder — h-chat-threads:<normalized-path> — an array of ChatThread, each holding the full conversation (messages, thought, fileChanges, todos) plus per-thread usage (token counts, context limit, turns).

Write triggers

TriggerLatencyWhere
Periodic auto-save30 sstateSync.startAutoSave()
App exit (beforeunload)immediatekeepalive fetch (≤64 KB body)
Every threads changenext renderthreads persist effect → saveThreads() + saveStateNow()
Agent turn ends (done)~50 msflushThreadsNow(usage) — final reply + usage flushed immediately
Agent run stoppedimmediatestop handler saveThreads() + saveStateNow()

saveStateNow() posts the entire mirrored state via POST /api/client/state; the server writes ~/.h/store/client-state.json. On startup the client calls GET /api/client/state and restores every persisted key into localStorage.

Files

FileRole
client/src/stateSync.tsGather/save/restore: saveState, saveStateNow, startAutoSave, loadPersistedState
client/src/panes/AgentConsole.tsxflushThreadsNow(usage) — immediate turn-end flush of messages + usage
server/index.tsGET/POST /api/client/state~/.h/store/client-state.json

Agent Command Catalog

Every shell command the agent can potentially issue via run_command or run_in_terminal. These are extracted from the agent's system prompt chunks and the detectProjectBuild() auto-detection logic in [server/agent.ts](file:///d:/Work Projects/H/server/agent.ts).

JavaScript / TypeScript

CommandUsageSource
npx tsc --noEmitType-check all files (preferred)LANG_JS + detectProjectBuild
npm run buildFull build via package.json scriptsLANG_JS + detectProjectBuild
npx eslint .Lint all filesLANG_JS
npm installInstall all project dependenciesLANG_JS
npm install <pkg>Install a specific packageLANG_JS

Python

CommandUsageSource
python -m py_compile <file>.pySingle-file syntax checkLANG_PYTHON
python -m compileall .Syntax check all .py filesLANG_PYTHON + detectProjectBuild
python -m pytestRun testsLANG_PYTHON
pip install -r requirements.txtInstall all project dependenciesLANG_PYTHON
pip install <pkg>Install a single packageLANG_PYTHON + SERVER_STARTUP

Go

CommandUsageSource
go build ./...Compile all packagesLANG_GO
go vet ./...Static analysisLANG_GO + detectProjectBuild
go test ./...Run all testsLANG_GO

Rust

CommandUsageSource
cargo checkFast compile check (no binary)LANG_RUST + detectProjectBuild
cargo buildFull compilationLANG_RUST
cargo testRun testsLANG_RUST
cargo clippyLint with extra warningsLANG_RUST

Java

CommandUsageSource
mvn compileMaven buildLANG_JAVA + detectProjectBuild
gradle buildGradle buildLANG_JAVA
gradle compileJavaGradle compile onlydetectProjectBuild
javac <File>.javaSingle file (no build tool)LANG_JAVA

C / C++

CommandUsageSource
gcc -Wall -Wextra <file>.c -o outputSingle C file with warningsLANG_C
g++ -Wall -Wextra <file>.cpp -o outputSingle C++ file with warningsLANG_C
cmake --build buildCMake projectsLANG_C + detectProjectBuild
makeMakefile projectsLANG_C + detectProjectBuild

Ruby

CommandUsageSource
ruby -c <file>.rbSyntax check (safe, no execution)LANG_RUBY + detectProjectBuild
bundle exec rake testRun tests via RakeLANG_RUBY
bundle exec rspecRun RSpec testsLANG_RUBY
bundle installInstall gem dependenciesLANG_RUBY
gem install <pkg>Install a single gemLANG_RUBY

PHP

CommandUsageSource
php -l <file>.phpSingle file syntax lintLANG_PHP
php -l *.phpLint all PHP filesLANG_PHP + detectProjectBuild
composer installInstall dependenciesLANG_PHP

Shell (Bash)

CommandUsageSource
bash -n <script>.shSyntax check without executingLANG_SHELL
shellcheck <script>.shStatic analysis (if installed)LANG_SHELL

Cross-language / Generic

CommandUsageSource
git status --porcelain -uStaged/unstaged file trackingServer SCM API
git log --max-count=20Recent commit historyServer SCM API
git diff -- <file>Show unstaged changes for a fileServer SCM API
git fetch --allFetch from all remotesServer SCM API
git pullPull latest changesServer SCM API
git pushPush local commitsServer SCM API

Project auto-detection (read_problems)

read_problems uses a two-tier approach:

  1. LSP-first: Reads real-time diagnostics from the language server (already visible in the terminal's Problems tab). This returns instant, zero-latency results exactly matching what the user sees — no shell command needed.

  2. Fallback command: If no LSP diagnostics are cached (e.g., the LSP hasn't started yet), it auto-detects the project type and runs a build/lint command:

Detection signalAuto-command
Cargo.toml existscargo check 2>&1
go.mod existsgo vet ./... 2>&1
pom.xml existsmvn compile 2>&1
build.gradle or build.gradle.kts existsgradle compileJava 2>&1
package.json + tsconfig.jsonnpx tsc --noEmit 2>&1
package.json with build scriptnpm run build 2>&1
package.json (no tsconfig, no build script)npx tsc --noEmit 2>&1
requirements.txt / pyproject.toml / setup.py / .py filespython -m compileall . 2>&1
Gemfile existsruby -c *.rb 2>&1
composer.json existsphp -l *.php 2>&1
Makefile existsmake 2>&1
CMakeLists.txt existscmake --build build 2>&1
None of the abovenpx tsc --noEmit / python -m compileall . / go vet ./... (general suggestion)

Server-specific (via run_in_terminal)

Commands the agent is instructed to launch in a real terminal tab:

FrameworkTypical commandMentioned in
Python (generic)python app.py / python server.pyrun_command block list
Flaskflask runrun_command block list
Djangopython manage.py runserverrun_command block list
FastAPIuvicorn main:apprun_command block list
Gunicorngunicorn app:apprun_command block list
Node.js (Express)node server.jsrun_command block list
npm scriptsnpm start / npm run devrun_command block list
Next.jsnext dev / next startrun_command block list
Viteviterun_command block list
Gogo run .run_command block list
Rustcargo runrun_command block list
Webpackwebpack-dev-serverrun_command block list
npx runnersnpx serve, npx vite, npx nextrun_command block list

Note: These server commands are BLOCKED in run_command and redirected to run_in_terminal. The agent is explicitly told to use run_in_terminal for all server start commands.

Troubleshooting by Language

The agent knows how to diagnose and fix errors for each language stack. Below is the guidance it follows — useful to understand what the agent will do when your build fails.

JavaScript / TypeScript

ScenarioToolCommand / Approach
Type-checkrun_commandnpx tsc --noEmit
Full buildrun_commandnpm run build (check package.json first)
Lint onlyrun_commandnpx eslint .
Missing modulerun_commandnpm install <pkg>
Runtime errors in browserbrowser_consoleAfter starting dev server, check console output
Failed API callsbrowser_request_errorsCheck for 404/500/CORS errors in the browser
Find a definitiongrepRegex search across project files

Python

ScenarioToolCommand / Approach
Syntax check (single file)run_commandpython -m py_compile <file>.py
Syntax check (all files)run_commandpython -m compileall .
Run testsrun_commandpython -m pytest
Install dependenciesrun_commandpip install -r requirements.txt or pip install <pkg>
Flask/Django runtime errorsbrowser_screenshotFlask debug mode shows full tracebacks in the browser; the screenshot image lets the vision model read the traceback directly and distinguish nav chrome from the actual error pane
HTTP errors from backendbrowser_request_errorsCheck for 500 errors and CORS issues
Find where a function is definedgrepdef <name> or class <Name>
Read stack tracesread_fileOpen the failing file at the line from the traceback

Go

ScenarioToolCommand / Approach
Compile checkrun_commandgo build ./...
Static analysisrun_commandgo vet ./...
Run testsrun_commandgo test ./...
Unused importedit_fileRemove the import line (Go forbids unused imports)
Find definitionsgrepfunc <Name> or type <Name>

Rust

ScenarioToolCommand / Approach
Fast compile checkrun_commandcargo check (preferred — no binary output)
Full buildrun_commandcargo build
Lintrun_commandcargo clippy
Run testsrun_commandcargo test

Java

ScenarioToolCommand / Approach
Maven compilerun_commandmvn compile
Gradle buildrun_commandgradle build
Single file compilerun_commandjavac <File>.java
Find class definitiongrepclass <Name>

C / C++

ScenarioToolCommand / Approach
Compile with warningsrun_commandgcc -Wall -Wextra <file>.c -o output
CMake buildrun_commandcmake --build build
Make buildrun_commandmake
Find function definitiongrepvoid <name>( or int <name>(

Ruby

ScenarioToolCommand / Approach
Syntax checkrun_commandruby -c <file>.rb
Install depsrun_commandbundle install
Run testsrun_commandbundle exec rspec or bundle exec rake test

PHP

ScenarioToolCommand / Approach
Syntax lintrun_commandphp -l <file>.php
Install depsrun_commandcomposer install

Shell (Bash)

ScenarioToolCommand / Approach
Syntax checkrun_commandbash -n <script>.sh
Static analysisrun_commandshellcheck <script>.sh

General troubleshooting flow

  1. Start the server (run_in_terminal) — user must Allow
  2. Check for build errors (run_command) — fixes go through edit_file / write_file
  3. Verify the page loads (browser_infobrowser_screenshot, then use the screenshot image to locate elements and click/type by their coordinates)
  4. Check browser runtime errors (browser_console, browser_request_errors)
  5. Read relevant source files (read_file) before making fixes
  6. Make targeted edits (edit_file — just send the lines that change)
  7. Rebuild and verify — repeat until clean

Avoiding tool hallucinations

The agent works with a fixed tool registry. To prevent it from inventing tools that don't exist:

  • Reading files → use read_file (never cat, head, tail)
  • Listing directories → use list_files (never ls, dir)
  • Finding files by name → use search_files (never find, locate)
  • Searching file contents → use grep (the tool, not the shell command)
  • Editing files → use edit_file (never sed, awk)
  • Writing files → use write_file (never echo >, cp)
  • Running commands → use run_command for short tasks, run_in_terminal for servers (never background with & or nohup)
  • Checking diagnostics → use read_problems (reads LSP diagnostics from the Problems tab — instant, no build command needed)
  • Dependency/structural queries → use read_graph (what exports X? who imports from Y? which imports are unused?) — much faster than grep for these
  • Starting servers → use run_in_terminal only (never run_command for python app.py, npm start, etc.)

MCP (Model Context Protocol)

H can act as an MCP server, exposing its filesystem, terminal, git, and system tools to any MCP-compatible client (Claude Desktop, Cursor, VS Code with Copilot, etc.).

Which transport to use

The configuration depends on how you're running H:

ScenarioTransportWhy
Development (source checkout)Stdio or SSEBoth work; stdio gives you project isolation
Electron desktop app (packaged)SSE onlyThe Express server already runs inside Electron — no extra process needed

In an Electron app: the H Express server starts inside the Electron main process. The MCP endpoints (/api/mcp, /api/mcp/sse) are available automatically on the server's assigned port. You do NOT need a separate process or a cwd pointing to the source code — just connect via SSE.

Development mode (source checkout)

When running H from source (npm run dev), you have both options:

Option A: SSE (simplest — no extra config)

Start the server, then point any MCP client at the running endpoint (check the console output for the port, or read ~/.h/ports/express-port):

{
  "mcpServers": {
    "H": {
      "url": "http://localhost:<port>/api/mcp/sse"
    }
  }
}

Works with Claude Desktop, Cursor, VS Code, and any SSE-compatible client.

Option B: Stdio (project isolation)

Run a separate process per project. The cwd points to the H source checkout so tsx and the server files are found:

npx tsx server/mcp-server.ts "D:\my-project"

Claude Desktop config (%APPDATA%\Claude\claude_desktop_config.json):

{
  "mcpServers": {
    "H": {
      "command": "npx",
      "args": ["tsx", "server/mcp-server.ts", "D:\\my-project"],
      "cwd": "D:\\Work Projects\\H"
    }
  }
}

Cursor config (Settings > MCP > Add Server):

{
  "mcpServers": {
    "H": {
      "command": "npx",
      "args": ["tsx", "server/mcp-server.ts", "${workspaceFolder}"],
      "cwd": "D:\\Work Projects\\H"
    }
  }
}

Electron desktop app (packaged)

When H is installed as a desktop app, the server starts automatically. The port is written to ~/.h/ports/express-port. Use SSE transport only — no command/cwd needed:

{
  "mcpServers": {
    "H": {
      "url": "http://localhost:<port>/api/mcp/sse"
    }
  }
}

The embedded Express server handles all MCP requests. The project root is automatically set to the currently open project folder in the H UI, so tools like read_file, grep, and run_command operate on the right project automatically.

Why stdio mode doesn't work well for packaged Electron apps:

  • There's no tsx runtime on the user's machine
  • The source files (server/mcp-server.ts) are compiled/bundled, not on disk
  • The Express server is already running inside Electron — spawning a second process is redundant

If you really need stdio from a packaged app, you can compile the MCP entry point to a standalone .cjs file and bundle it with the app. But SSE is the intended path.

MCP tools

The following tools are exposed via MCP:

ToolDescription
read_fileRead a file with line numbers or list a directory
write_fileCreate or overwrite a file
edit_fileTargeted string replacement in a file
list_filesList files and directories at a path
search_filesFind files/folders by name pattern
grepSearch file contents with regex
run_commandExecute a shell command (sandboxed, no permission needed)
create_directoryCreate a directory (and parent dirs)
delete_fileDelete a file or directory (recursive)
rename_fileRename or move a file or directory
git_statusGet staged and unstaged git changes, current branch
git_logGet recent commit history
git_diffGet the diff for a specific file
system_infoGet CPU, memory, disk, OS details

Protocol details

H implements MCP protocol version 2024-11-05 with JSON-RPC 2.0:

  1. Initialize — Client sends initialize → Server returns capabilities and server info
  2. List tools — Client sends tools/list → Server returns tool definitions with JSON Schema
  3. Call tool — Client sends tools/call → Server executes the tool and returns { content: [{ type: "text", text: "..." }] }

The server only exposes tools capability — no resources or prompts.

Example MCP exchange

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"H","version":"1.0.0"}}}

→ {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}

→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"server/index.ts","limit":10}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"   1| import \"dotenv/config\";\n..."}],"isError":false}}

Project Structure

H/
├── .env.example                     # Template for environment variables (DeepSeek API key)
├── package.json                     # Root deps (Express, node-pty), scripts (dev, test, desktop:build, desktop:pack)
├── package-lock.json
├── tsconfig.json                    # TypeScript config for server (CommonJS output, Node moduleResolution, strict=false, vitest globals, @types/node)
├── vitest.config.ts                 # Vitest runner config (node env, 30s timeout)
├── README.md                        # Landing page with USP comparison
├── README_CN.md                     # Chinese landing page
├── ARCHITECTURE.md                  # Full architecture documentation
├── ARCHITECTURE_CN.md               # Chinese architecture documentation
├── LICENSE                          # AGPL-3.0

├── build/
│   └── icon assets (ico, png, svg)  # Electron app icons

├── scripts/
│   ├── embed-icon.js                # Embeds icon into the Electron executable (post electron-builder --dir stage)
│   ├── generate-icon.js             # Generates icon from SVG source
│   └── patch-electron-name.js       # Patches electron.exe -> H.exe + AppUserModelID before desktop runs

├── dist/                            # Build output (generated, NOT committed)
│   └── server/                      #   Compiled server (output of `tsc -p tsconfig.json` + bootstrap copy)
│       ├── bootstrap-packaged.cjs   #     --require preamble: registers both node_modules roots before index.js runs
│       └── index.js                 #     Express server (CommonJS). Entry used by packaged H.exe.

├── server/
│   ├── index.ts                     # Express server entry: API routes, WebSocket terminal, agent SSE streaming, MCP, browser proxy, system stats
│   │                                #   listenInRange(51734, 20) => fixed port range; stdout banner parsed by main process
│   ├── bootstrap-packaged.cjs       #   SOURCE of dist/server/bootstrap-packaged.cjs (copied by desktop:build-server)
│   ├── agent.ts                     # Agent core: 27 tool definitions, 11 sub-agent profiles, delegate_task, agentLoop/Stream/StepByStep, permission gating, ITR system prompt builder, history compaction
│   ├── deepseek.ts                  # DeepSeek API client: blocking + streaming chat w/ tool-calling, embeddings, KV cache tracking, usage reporting
│   ├── terminalManager.ts           # Terminal session manager: PTY (node-pty) and pipe fallback, WebSocket I/O, venv auto-activation, localhost URL detection
│   ├── lsp.ts                       # LSP client: spawns language servers (pyright, gopls, etc.), SSE diagnostic streaming, 30+ languages
│   ├── mcp.ts                       # MCP server: JSON-RPC handler, tool set for external AI clients, stdio + SSE transport
│   ├── mcp-server.ts                # Standalone MCP server entry point (stdio mode)
│   ├── memory.ts                    # File-based persistent memory store (~/.h/memory): keyword search + session/topic logging, used by agent remember/recall/forget tools
│   ├── cryptoStore.ts               # AES-256-GCM encrypted API key storage (~/.h/store/api-keys.enc)
│   ├── hPaths.ts                    # Centralized path resolution for ~/.h/ directory structure
│   ├── fileTracking.ts              # Smart file tracking: auto-detects Git vs fs.watch watcher mode, mid-session Git detection, snapshot/patch file tree context
│   ├── fileTrackingStore.ts         # JSON-backed file metadata cache (~/.h/store/file-tracking.json) for watcher mode
│   ├── knowledgeGraph.ts            # Codebase knowledge graph builder: dir/file/symbol nodes, CONTAINS/EXPORTS/IMPORTS/IMPORTS_SYMBOL edges, import classification + unused detection, .kg/.txt serialization
│   └── __tests__/
│       ├── agent.tooldefs.test.ts    # Tool definition schema validation
│       ├── agent.fs.test.ts          # Filesystem tool execution tests
│       ├── agent.command.test.ts     # Command execution tests
│       ├── agent.loop.test.ts        # Agent loop integration tests
│       └── api.test.ts              # API endpoint integration tests

├── client/
│   ├── package.json                 # Client deps (React 18, Monaco, xterm.js), Vite + TypeScript
│   ├── vite.config.ts               # Vite dev config: hProxyPlugin scans Express range 51734–51753 via TCP probes
│   │                                #   + /api /ws /_browser /settings /resources proxying; writeVitePortPlugin writes dev Vite port to $TMP/h-ports/
│   ├── tsconfig.json                # Client TypeScript config (ES2020, DOM, react-jsx)
│   ├── index.html                   # SPA entry: mounts React app in <div id="root">
│   ├── public/
│   │   └── icon.svg                 # App icon SVG
│   └── src/
│       ├── main.tsx                 # React DOM entry: renders <App />
│       ├── App.tsx                  # Root component: folder picker, session state, resizable layout (editor + agent console)
│       ├── App.css                  # Global dark-theme styles: pane layout, editor chrome, agent cards, sub-agent color coding (11 agent types), welcome screen
│       ├── electron.d.ts            # Type declarations for window.hDesktop bridge and <webview> JSX
│       ├── vite-env.d.ts            # Vite client type declarations
│       ├── stateSync.ts             # Client state persistence: mirrors all localStorage to ~/.h/store/client-state.json (survives reinstalls)
│       ├── panes/
│       │   ├── EditorPane.tsx       # Main editor: Monaco tabs, file tree, browser tab strip, terminal, menu bar, status bar
│       │   ├── AgentConsole.tsx     # Agent chat UI: streaming messages, diff previews, permission prompts, tool cards with agent color coding, markdown rendering
│       │   ├── TerminalPane.tsx     # xterm.js terminal: WebSocket-backed PTY, Ctrl+click links, scrollback, agent bridge
│       │   ├── FilesPanel.tsx       # File explorer tree: virtual files + backend FsEntry, create/rename/delete
│       │   ├── BrowserView.tsx      # Embedded browser: iframe proxy, coordinate-based agent APIs (pixel screenshot + click/type at x,y), no DOM indexing
│       │   ├── MenuBar.tsx          # Dropdown menus: File, Edit, View, Run, Help with keyboard shortcuts
│       │   ├── StatusBar.tsx        # Status bar: cursor position, encoding, indent, language, LSP errors, memory count
│       │   ├── ScmPanel.tsx         # Source control: git status, commit log, fetch/pull/push, diff
│       │   ├── NameDialog.tsx       # Modal dialog for create/rename files and folders
│       │   ├── PathDialog.tsx       # Modal dialog for manually opening a folder path
│       │   ├── AgentTerminalBridge.ts # Bridge: agent commands → real terminal execution
│       │   ├── fileModel.ts         # VFile type, detectLanguage(), file/folder icon helpers
│       │   └── browserFs.ts         # Browser File System API: pickAndEnumerateFolder, readFile, writeFile
│       └── hooks/
│           └── useResizable.tsx     # Drag-to-resize panel splitter hook

└── electron/
    ├── main.cjs                      # Electron main process: splash loader, embedded server spawn (ELECTRON_RUN_AS_NODE=1, windowsHide, shell:false, cwd to resources dir),
    │                                 #   findLiveServerPort: stdout banner regex + TCP 51734..51753 + /api/health PID verification,
    │                                 #   BrowserWindow + IPC (folder/file picker, geo, permissions), browser session,
    │                                 #   startup log -> ~/.h/logs/startup-*.log (survives NSIS installer console teardown)
    ├── preload.cjs                   # Preload bridge: exposes window.hDesktop (openFolder, openFile, onBrowserOpenUrl, setSitePermissions)
    ├── browser-preload.cjs           # Browser webview preload: geolocation bridge, _blank link interception
    └── native-location.cjs           # Windows geolocation via PowerShell GeoCoordinateWatcher

Testing

H includes an automated test suite using Vitest. Tests cover all agent tools, the agent loop (with mocked DeepSeek API), tool schema validation, and API endpoints.

Running tests

# Run all tests once
npm test

# Run tests in watch mode (re-run on file changes)
npm run test:watch

# Run tests with coverage report
npm run test:coverage

# Run a specific test file
npx vitest run server/__tests__/agent.fs.test.ts

Test structure

server/__tests__/
├── agent.fs.test.ts          # Filesystem tools: read_file, write_file, edit_file,
│                             #   list_files, search_files, grep, create_directory,
│                             #   delete_file, rename_file, write_todos (34 tests)
├── agent.command.test.ts     # Command tools: run_command, run_in_terminal,
│                             #   read_command_output (19 tests)
├── agent.loop.test.ts        # Blocking and streaming agent loops with mocked
│                             #   DeepSeek API responses (14 tests)
├── agent.tooldefs.test.ts    # Tool schema validation: required fields,
│                             #   no duplicate names, property integrity (6 tests)
└── api.test.ts               # Express endpoint integration tests: health,
│                             #   agent chat, filesystem APIs, project detection,
│                             #   system stats (12 tests)

Test layers

LayerWhat's testedMock strategy
Tool definitionsEvery tool has valid JSON Schema, no duplicate names, required params have matching propertiesNone (static validation)
Filesystem toolsrunFsTool() for each filesystem tool with real temp directoriesReal filesystem
Command toolsrun_command executes, blocks servers, returns exit codes; read_command_output pagination and filteringReal spawn
Agent loopagentLoop() and agentLoopStream(): tool selection logic, multi-turn loops, browser/permission handoff, iteration limits, reasoning content passthroughMocked DeepSeek API
API endpointsExpress routes: /api/chat/agent, /api/chat/agent/stream, /api/health, /api/system/stats, /api/project/detect, filesystem CRUD, session cleanupMocked DeepSeek, real supertest

Writing new tests

  1. Tests use Vitest globals (describe, it, expect, vi, beforeEach, afterEach)
  2. For agent loop tests, mock chatDeepSeekTool or chatDeepSeekToolStream from server/deepseek.ts to return controlled responses
  3. Filesystem/command tests use runFsTool() with real temp directories created via fs.mkdtempSync()
  4. API tests use supertest against the exported app from server/index.ts
  5. Clean up temp directories in afterEach hooks

Token Optimization (ITR + Context Caching + Live Compaction)

H uses four layers to reduce token usage and API costs when talking to DeepSeek:

1. Instruction-Tool Retrieval (ITR)

Instead of sending the entire system prompt on every agent turn, the prompt is broken into 14 themed chunks in server/agent.ts, each with a set of trigger keywords. At each turn, buildSystemPrompt() selects only the chunks relevant to the current conversation context.

Chunk registry

Each chunk is a constant string paired with an array of trigger keywords:

Chunk (id)SizeTrigger keywords (partial)Decision
CORE_RULES~700 wordsAlways included
browser~400 wordsbrowser_, DOM, navigate, click, type, form, modal, dialog, dropdown, autocomplete, hoverScore ≥ 2, or auto-included if any browser_* tool has been called
build_fix~120 wordsbuild, compile, error, fix, edit_file, write_file, read_problems, run_command, syntaxScore ≥ 2
lang_js~200 words.ts, .tsx, .js, .jsx, package.json, typescript, node, npm, react, vite, import Score ≥ 2
lang_python~250 words.py, python, pip, flask, django, traceback, ModuleNotFoundError, uvicornScore ≥ 2
lang_go~100 words.go, go.mod, go build, go vet, go test, go runScore ≥ 2
lang_rust~100 words.rs, cargo, Cargo.toml, rustc, rust, clippyScore ≥ 2
lang_java~80 words.java, pom.xml, build.gradle, maven, mvn, gradle, javacScore ≥ 2
lang_c~80 words.c, .cpp, .h, CMakeLists.txt, gcc, g++, cmake, makefileScore ≥ 2
lang_ruby~80 words.rb, Gemfile, ruby, rake, rspec, bundle, gem, railsScore ≥ 2
lang_php~60 words.php, composer.json, php, laravel, symfony, wordpressScore ≥ 2
lang_shell~60 words.sh, .bash, shellcheck, #!/bin/bash, bash , MakefileScore ≥ 2
lang_general~80 wordsAlways included
server_startup~350 wordsrun_in_terminal, npm start, npm run dev, flask run, uvicorn, EADDRINUSE, port, listenScore ≥ 2
diagnostics~200 wordsrun_command, read_problems, read_command_output, terminal, sandbox, build, compile, test, lint, errorScore ≥ 2

Full chunk registry with every trigger keyword lives in [PROMPT_CHUNKS](file:///d:/Work Projects/H/server/agent.ts#L1743-L1862).

Selection algorithm

At each agent turn, buildSystemPrompt() in [agent.ts](file:///d:/Work Projects/H/server/agent.ts#L1907-L1959) runs this flow:

1. Start with CORE_RULES (always present)

2. Build a combined text blob from:
   • All message content (user, assistant, tool results)
   • Tool call names extracted from assistant messages
   • IDE context (open files, diagnostics)

3. For each optional chunk:
   count = 0
   for each trigger keyword:
       if keyword (case-insensitive) appears in combined blob:
           count += 1
   if count >= 2 → INCLUDE chunk
   if count < 2  → SKIP chunk

4. Special rule: browser chunk gets auto-included (boost = +5)
   if any browser_* tool has been called this session,
   regardless of keyword matches

5. Append IDE context footer

6. Log selection stats to console:
   [ITR] prompt: 6142 chars (~1535 tokens) | 5 chunks selected, 9 skipped
   [ITR]   included: build_fix(s:6), lang_js(s:7), diagnostics(s:4)
   [ITR]   skipped:  browser(s:0), lang_python(s:0), lang_go(s:0), ...

Concrete example: TypeScript project, no browser interaction

The combined text blob for a typical TypeScript turn contains .ts, package.json, typescript, edit_file, run_command, build, error, read_problems, import , etc.

Chunk            Triggers matched              Score   Decision
─────────────────────────────────────────────────────────────────
CORE_RULES       (always)                       —      ✓ INCLUDE
browser          none                           0      ✗ SKIP
build_fix        build, error, edit_file,       6      ✓ INCLUDE
                 fix, run_command, syntax
lang_js          .ts, package.json,             7      ✓ INCLUDE
                 typescript, import, npx,
                 tsc, react
lang_python      none                           0      ✗ SKIP
lang_go          none                           0      ✗ SKIP
lang_rust        none                           0      ✗ SKIP
lang_java        none                           0      ✗ SKIP
lang_c           none                           0      ✗ SKIP
lang_ruby        none                           0      ✗ SKIP
lang_php         none                           0      ✗ SKIP
lang_shell       none                           0      ✗ SKIP
lang_general     (always)                       —      ✓ INCLUDE
server_startup   none                           0      ✗ SKIP
diagnostics      run_command, read_problems,    4      ✓ INCLUDE
                 build, test
─────────────────────────────────────────────────────────────────
Result: 5 chunks included, 9 skipped

~600 words sent vs ~8,000 if all 14 chunks → ~92% reduction in system prompt size.

Interaction with DeepSeek prefix caching

Because buildSystemPrompt() produces the same output when the conversation context is stable (same project, same language, same tool patterns), the system message stays identical across consecutive turns. DeepSeek's server-side KV cache then reuses the cached prefix tokens — so the system prompt costs zero additional tokens on cache hits beyond the first turn. ITR keeps the prompt small and stable, which makes cache hits more frequent.

2. Context Caching

DeepSeek API supports automatic prefix caching: when consecutive requests share an identical message prefix (the system message), the server reuses the KV cache for those tokens — reducing both cost and latency.

H leverages this in two ways:

  • Stable system messages: Because buildSystemPrompt() produces the same output for the same context, the system message stays stable across turns where the detected project stack doesn't change. DeepSeek hits the prefix cache automatically for these consecutive calls.
  • Local heuristic tracking: server/deepseek.ts computes a context ID from the system message content and logs a best-effort HIT / MISS line based on whether the current system-prompt hash matches the previous request.
  • API-backed cache usage: H also reads the actual DeepSeek usage payload and extracts:
    • prompt_tokens
    • completion_tokens
    • total_tokens
    • prompt_cache_hit_tokens
    • prompt_cache_miss_tokens

For streamed tool-calling requests, server/deepseek.ts enables stream_options.include_usage so the final SSE chunk includes usage data. That produces a console log like:

[cache-api] stream model=deepseek-v4-flash prompt=1234 completion=456 total=1690 cache_hit_tokens=900 cache_miss_tokens=334 hit_rate=73%

Important distinction:

  • The old [cache] ... HIT/MISS ... line is a local H heuristic based on prompt-hash reuse.
  • The new [cache-api] ... line is based on actual DeepSeek API usage fields.

No extra cache-control API parameters are needed — DeepSeek handles prefix caching transparently on the server side.

2b. Sub-agent Prefix Caching

Each sub-agent (delegate_task) normally starts with a fresh messages array — just the system prompt and the task string. Since every delegation has a unique task, turn 1 of every sub-agent is a cache miss, even when delegating the same agent type repeatedly (e.g., multiple browser sub-agents).

To improve this, H stores a shared message prefix per agent type on the parent session. After a sub-agent completes, its task and summary are appended to the prefix. The next delegation of the same type prepends these older task/summary pairs before the new task, making the API message prefix identical across calls:

Before (5 browser sub-agents, 3 turns each):
  Sub-agent 1: [sys, task1]                  ← MISS
  Sub-agent 2: [sys, task2]                  ← MISS  (task2 ≠ task1)
  Sub-agent 3: [sys, task3]                  ← MISS
  → 5 misses (one per delegation start)

After (same scenario):
  Sub-agent 1: [sys, task1]                  ← MISS (first ever)
  Sub-agent 2: [sys, task1, summary1, task2] ← HIT on [sys, task1, summary1]
  Sub-agent 3: [sys, task2, summary2, task3] ← HIT on [sys, task2, summary2]
  → 1 miss only (first delegation)

What's stored (in AgentState.subAgentPrefix):

FieldContentWhy
KeyAgent type string ("browser", "code-writer", etc.)Per-type isolation — browser sub-agents share with each other, not with code-search
MessagesLast 2 task/summary pairs (4 messages max)Bounded growth; never stores file contents (read_file/grep results), so project changes don't cause stale context
Task contentTruncated to 500 charsKeeps prefix compact
Summary contentTruncated to 1000 charsKeeps prefix compact

Where it's applied:

  • runSubAgentStream (agent.ts) — streaming path used by the SSE agent loop
  • runSubAgent (agent.ts) — non-streaming path (researcher tasks)
  • resumeSubAgent does NOT store prefix — it resumes an already-paused sub-agent, so the window is unchanged

This is a low-risk optimization: only task/summary pairs are shared, never tool results containing project file contents. The worst case is 4 stale summary lines in the prefix, which act as lightweight context hints rather than authoritative information.

3. Rolling History Compaction

Long-running agent sessions now compact older plain-text turns on the server before building the next model request.

  • Only older plain chat turns are compacted: user messages and non-tool assistant replies.
  • The most recent plain turns stay verbatim so the model still sees the latest local context.
  • Older plain turns are merged into a bounded history summary stored in the in-memory agent session.
  • Tool-call ordering is preserved: assistant tool calls, tool results, pending permission state, and deferred file-accept/reject state are kept as structured messages.

This means the live prompt no longer grows linearly with every user/assistant exchange in long sessions.

Current defaults in server/agent.ts:

SettingValueEffect
HISTORY_COMPACTION_TRIGGER_MESSAGES24Start compacting when the in-memory session grows beyond this many messages
HISTORY_COMPACTION_TRIGGER_TOKENS10000Also compact when the rough token estimate crosses this threshold
HISTORY_PLAIN_MESSAGES_TO_KEEP6Keep the latest plain turns verbatim
HISTORY_SUMMARY_CHAR_BUDGET2400Bound the rolling summary size

4. Tool Result Distillation

Some tool outputs are much larger than what the model usually needs on the next turn.

H now distills bulky command/build output before storing it back into the agent transcript:

  • run_command stores a compact summary instead of the full raw output
  • run_in_terminal stores a compact summary (key error/success lines) instead of the full terminal log — the full output is cached for read_command_output
  • read_problems reads LSP diagnostics from the Problems tab (live, zero-latency); falls back to a compact build-check summary if no LSP cache is available
  • The summary keeps the most important lines (errors, warnings, failures, URLs, success markers)
  • The full raw command output is still cached in the command-output store and can be re-read later with read_command_output

This cuts repeated replay of large terminal/compiler logs while keeping the raw output available on demand.

5. Context Token Estimation

The agent footer shows a live estimate of context usage: ~N / M tokens (X%) · T turns. This is calculated on the server each agent turn and sent to the client via the SSE done event.

H now also sends cumulative DeepSeek API usage in that same done payload when available:

  • requestCount
  • promptTokens
  • completionTokens
  • totalTokens
  • promptCacheHitTokens
  • promptCacheMissTokens

The footer keeps the ring based on the local estimated-context value, and adds cache status when the API returned cache usage. The compact label becomes:

X% · Tt · cYY%

Where cYY% is the cumulative cache hit rate derived from:

promptCacheHitTokens / (promptCacheHitTokens + promptCacheMissTokens)

The footer tooltip includes the fuller API totals: request count, prompt/completion/total tokens, and cache hit/miss token counts.

How it's calculated

estimateStateTokens() in server/agent.ts walks every field of every message:

totalChars =
  Σ messages (
    content.length          // user messages, assistant replies, tool results
    + tool_call_id.length   // UUIDs linking tool calls to results
    + name.length           // function names (e.g. "read_file", "grep")
    + reasoning_content?.length  // DeepSeek chain-of-thought (R1/reasoner models)
  )
  + historySummary?.length  // compacted older conversation turns
  + systemPromptChars       // ITR-selected prompt chunks (counted once per turn)

estimatedTokens = round(totalChars / 4)

Each turn, buildOpenAiMessages() calls estimateStateTokens() to check against HISTORY_COMPACTION_TRIGGER_TOKENS (10,000). The final estimate is also sent to the client via the SSE done event for the usage ring in the footer. Separately, the server accumulates actual DeepSeek API usage across the run and attaches those totals to the same usage object.

Accuracy

FactorNote
chars / 4Rough heuristic. DeepSeek's byte-level BPE tokenizer varies: code/text is typically 2-3 chars/token, CJK ~1 char/token. Can be off by up to 2x.
System promptCounted. Built from ITR chunks (3-15K chars / 0.75-3.75K tokens).
reasoning_contentCounted. DeepSeek R1/reasoner models produce verbose chain-of-thought.
Console contextNOT counted. The IDE's diagnostic/terminal context is small and passed separately for ITR selection.
NOT_EXECUTED injectionsNOT counted. These are injected by buildOpenAiMessages at API-call time and not stored in state.messages.
API token usageSeparate from the estimate. Comes from DeepSeek's usage payload and may not appear if the provider omits usage for a given response.

Context limit

The contextLimit is set dynamically based on the model name:

Model patternContext window
deepseek-chat (V3), deepseek-reasoner (R1)128,000 tokens
Models containing v4, pro, or flash1,000,000 tokens
Unknown / custom128,000 tokens (default)

Cumulative turns

The turns counter accumulates across the entire session, not per-response. The server sends turns = state.iteration (iterations in that agent turn), and the client sums them into totalTurnsRef.

Architecture

                ┌─────────────────────────────┐
                │    buildSystemPrompt()      │
                │  scans messages + context   │
                │  selects relevant chunks    │
                └──────────────┬──────────────┘

                    ┌──────────▼──────────┐
                    │ Rolling compaction   │
                    │ + history summary    │
                    └──────────┬──────────┘

                    ┌──────────▼──────────┐
                    │ Tool result          │
                    │ distillation         │
                    └──────────┬──────────┘

                    ┌──────────▼──────────┐
                    │ Mini system prompt   │
                    │ + compact transcript │
                    └──────────┬──────────┘

                    ┌──────────▼──────────┐
                    │ deepseekFetch()      │
                    │ + cacheContextId     │
                    │ → DeepSeek API       │
                    └─────────────────────┘

Files:

  • server/agent.tsbuildSystemPrompt(), rolling history compaction, tool-result distillation, prompt assembly
  • server/deepseek.tsdeepseekFetch() with cacheContextId parameter, cache metrics logging