Skip to content

Repository files navigation

finestra

Claude draws its own interface, live, on your machine.

Asking for a panel, getting one, clicking a heading to leave a note, and the panel coming back changed

Ask for a panel. Claude writes one as an ordinary HTML file. The page patches it in place the moment the file lands, so scroll position, form input and open sections survive every update. Click any part of it, say what should change, and it comes back changed without a reload.

The terminal stays the engine room. The browser is where you read, look, and point at things. Nothing is sandboxed and nothing is uploaded, because the page and the session are the same machine.

What you get

  • Panels that change under you. Ask for a chart, a map of the repo, three design directions. Claude writes the file, the page morphs it in place, and what you were looking at stays where it was. Panels are tabs, and they outlive the turn that made them.
  • Point at anything and say what's wrong. Click an element, select the words you mean, attach a note. Panels and the window's own furniture are both fair game, so "make this heading smaller" works on a chart and on the top bar alike. Notes accumulate and go over as one pass.
  • A window you can rewrite. The top bar, transcript, composer and tabs are files in your workspace. Ask Claude to change them, or open them yourself. CSS swaps live.
  • The everyday things too. A transcript with expandable tool calls and a subagent's whole run nested inside the call that launched it, the /diff you wanted with a comment on any line, and plans you reorder and strike before approving.

Install

Type this in Claude Code:

/plugin marketplace add Goodeye-Labs/finestra
/plugin install finestra@goodeye-labs
/reload-plugins

Then /finestra. There is nothing to clone, nothing to symlink, and no settings file to edit. Stop and the approval gate arrive already wired, because the hook ships with the plugin.

Requires Node 20+, Claude Code 2.1.220 or newer, and curl. macOS and Linux; Windows means WSL. Nothing to build and no dependencies, so there is no npm install step.

The context and cost meter is the one thing a plugin cannot configure for you. It is optional, and Hooks says how to add it.

To update: /plugin update finestra@goodeye-labs. To have it update itself, run /plugin, open Marketplaces, select goodeye-labs and choose Enable auto-update.

finestra doctor reports what is wired and what is not, and says which of it is optional. To remove everything: /plugin uninstall finestra@goodeye-labs.

First run

Run /reload-plugins, or start a new session, since a session already running has not seen the plugin yet. Then type:

/finestra

You get a URL and a browser window. The left column is already your conversation. Now ask for something that wants a picture:

show me a map of this repo as a panel: top-level directories, what each one
holds, and the biggest file in each

A tab appears. Then hit Review in the top bar, click any heading in that panel, type "make this smaller", and send. The panel changes in place, and you never touched the terminal.

That round trip is the whole product. Everything below is detail.

This is what a working window looks like. Your conversation on the left, with tool calls you can expand, and whatever Claude drew on the right.

The window: transcript with expandable tool calls on the left, a rendered panel on the right

If asking for something visual doesn't open a window, type /finestra directly. Explicit invocation always works.

Reviewing changes

The Changes tab is the CLI's /diff. Hover a line to comment on it, and the batch goes back carrying the file, line, hunk and code, which is what makes it something Claude can act on rather than a note about a page. A commented line stays marked.

The Changes tab, with a comment being attached to a line of the diff

Underneath it is the checkpoint list, rebuilt from what Claude Code records before each prompt. It is read-only, since restoring is /rewind in the terminal.

Pointing at things

Hit Review, click anything, attach a note. Panels and the window's own furniture are both fair game, so "make this heading smaller" works on a chart and on the top bar alike. Pins accumulate and go over as one batch when you send, so a review reads as one pass instead of interrupting on every click. Select words first and the note is about the phrase rather than the whole element.

The composer

/ opens a menu of everything you can invoke, read from your skills and installed plugins, so a skill added on disk shows up without finestra knowing about it. Built-in commands are listed but marked terminal-only, because nothing outside the CLI can dispatch /compact.

@ completes paths. walks your real prompt history for this directory, ^R searches it. Pasted and dropped images land in the session's assets and go into the message as a path Claude can read. ⌘K is a command palette over all of it. ⌘F finds in the conversation.

Type while a turn is running and the message is held rather than sent, so it stays editable until the turn ends.

What it reads, writes, and sends

Nothing leaves your machine. No network calls, no telemetry, no dependencies, no MCP server. The port binds to 127.0.0.1, and curl is used once by the launcher against that port to find the server it just started.

It reads your session and subagent transcripts, your prompt history, which sessions are live, and your skills and installed plugins to fill the / menu. In your project it runs reporting-only git commands for the Changes tab.

It writes panels, layout, assets and the inbox into a scratch workspace keyed to this session, never your repo. Prompts typed in the window are appended to ~/.claude/history.jsonl, the same file the terminal uses, so works in both. That is the one file finestra writes that Claude Code also writes, so the append is bounded and set FINESTRA_NO_HISTORY=1 to turn it off.

A human sends every message. finestra never prompts your session on its own, and it holds no credential: it reads files Claude Code already wrote and writes files Claude Code reads. Every model call is made by your own Claude Code.

Panel HTML is injected without sanitizing, deliberately: Claude authoring arbitrary HTML is the entire point, and it already has full filesystem access in your session. So don't point FINESTRA_STATE at a directory you didn't generate, and don't expose the port. Chat text is a different matter and is escaped before a small markdown subset is applied.

Closing it, and getting it back

The window dies with its session, and the launcher clears workspaces whose session has ended. To close one sooner, end the session or kill the server.

Closed the tab by accident? Run /finestra again. It reopens the same window rather than starting a second one.

To uninstall: /plugin uninstall finestra@goodeye-labs. That takes the hook with it. Drop the statusLine entry too if you added one.

Hooks (optional)

The interrupt hook needs nothing from you. It ships in the plugin's own hooks/hooks.json, so installing wires it and uninstalling removes it.

interrupt.sh is what makes Stop land mid-turn: a message from the window queues behind the running turn by design, and a PreToolUse hook is the only thing that runs while a turn is in flight. It is global, so it runs before every tool call in every project, and when finestra is idle it is two file tests and an exit.

The context meter needs one line. A plugin's own settings may only carry a couple of keys, and statusLine is not among them, so this is the one thing left to add by hand. Ask Claude for it, or merge this into ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "sh -c 'exec node \"$(ls -d \"$HOME\"/.claude/plugins/cache/*/finestra/*/ | sort -V | tail -1)hooks/statusline.mjs\"'",
    "refreshInterval": 5
  }
}

That resolves the newest installed copy every time it runs. Naming one version's path directly also works, and then quietly stops working at the next update, because each version is installed beside the last rather than over it.

Merge it into what is already there rather than replacing the file. Claude Code picks it up without a restart. Skip it and everything else still works: the window says plainly that the meter is missing rather than showing a blank gauge.

statusline.mjs feeds context usage, cost and rate-limit headroom, none of which is in the transcript. It still prints a status line, because it is one.

Holding tool calls for approval (optional)

With the PreToolUse hook installed, the palette offers to hold shell commands, or every tool call, until you say so. This is finestra's own checkpoint rather than a view of Claude Code's permission prompt, which is drawn by the terminal and never reaches disk.

The gate only ever tightens. It either denies a call or says nothing, and it cannot approve past a rule that would have stopped one. In a session running with prompts turned off, it reinstates a human checkpoint that would otherwise not be there.

Holding a turn is the kind of feature that wedges a session when it goes wrong, so it is off unless you arm it, it comes down when the window closes, waiting always ends, and every failure path allows. A broken gate behaves exactly like an absent one.

Advanced

Which session a window shows

One window, one session, bound when the server starts and never moved. There is no switcher: reaching a session means something inside it is watching its inbox, and nothing outside can make a cold session start watching, so a list of sessions you cannot talk to is worse than no list.

Two sessions in one directory do not collide. Separate workspaces, inboxes, shells, assets and ports, all keyed to the session id.

Where the files live

The workspace defaults to scratch: Claude Code's own per-project temp root when that exists, otherwise ${TMPDIR:-/tmp}/finestra/<slug>. A window belongs to one session and dies with it, so leaving it in a working tree would litter every repo with state nothing outlives the conversation to use.

Set FINESTRA_STATE to keep panels somewhere durable instead. A directory you name is yours, and is never swept.

Environment

FINESTRA_PORT (default 4747, stepping up to the next free port unless you set it), FINESTRA_PROJECT (default cwd), FINESTRA_SESSION (the session to show), FINESTRA_STATE (where the window's files live), and FINESTRA_NO_OPEN (start without opening a browser).

Starting a server by hand

The launcher is the normal path, because it knows the session id and ties the server's life to it. To run one yourself, name the session:

FINESTRA_STATE=/tmp/finestra FINESTRA_SESSION=<id> node src/server.mjs

Give it FINESTRA_STATE, or it falls back to .finestra/ inside the project. A server started this way has no owner, so it keeps running after the session ends and you have to stop it yourself.

Everything on screen is editable

On first run the window copies its own interface into the workspace and serves it from there. The top bar, transcript, composer and tabs are all files you can change, by asking Claude or by opening them. CSS swaps live. A file you have edited is never overwritten by an upgrade, and POST /shell/reset restores the shipped version.

Panels inherit the theme through :host([data-theme='dark']) rather than a media query, because the toggle in the top bar has to beat the OS setting.

Screenshots and tests

scripts/demo.mjs serves a window on a synthetic session, and scripts/shot.mjs photographs one headlessly in its own browser profile:

node scripts/demo.mjs                                   # prints a URL
node scripts/shot.mjs http://localhost:4788 out.png     # from another shell
npx playwright install chromium   # once, for the UI suite's real browser
npm test

Three suites run in order and the first failure stops the rest: the markdown subset, then the server against a synthetic transcript, then the real page driven over CDP. None of them touch ~/.claude or any live session.

npm run test:core stops after the first two, which need no browser. The UI suite and shot.mjs need Node 22+ for a global WebSocket, while the product itself runs on 20, which is what CI checks.

Under the hood

Zero dependencies, no MCP server, and nothing in the critical path that depends on a research-preview feature.

terminal (claude)                     browser (127.0.0.1:4747)
      │                                     │
      │ writes transcripts                  │ SSE ▲    │ POST ▼
      ▼                                     │         │
~/.claude/projects/…/<session>.jsonl  ──────┤         │
~/.claude/projects/…/subagents/*.jsonl ─────┤         │
                                     server.mjs       │
<state>/sessions/<id>/panels/*.html ─watch──┤         │
<state>/sessions/<id>/shell/*       ─watch──┘         │
<state>/sessions/<id>/inbox.jsonl   ◄─────────────────┘
      │
      └── tail -f, via Claude Code's Monitor tool ──► back into the session

Claude writes a panel with the ordinary Write and Edit tools, fs.watch picks it up, and the page morphs it into the DOM with Idiomorph rather than replacing it.

The server tails the transcript JSONL Claude Code already writes, which carries messages, tool calls and results, permission mode and which skill drove each message, so none of the display needs Claude's cooperation. Subagents are not in that file and write their own beside it, which are tailed too.

Coming back the other way, typed messages, pins and answers are appended to the session's inbox.jsonl, and Claude watches its own inbox with a persistent Monitor. Questions work the same way: AskUserQuestion blocks on the terminal's prompt, so Claude writes the question to a file and ends its turn instead, and the answer returns through the inbox with nothing blocking.

The reason all of it is files: the server holds no durable state, so killing it or reloading the tab costs nothing, and you can hand-edit a panel because the watcher doesn't care who wrote the file.

Limits

v0.1, one session per window. Text arrives in paragraphs rather than word by word, because token streaming only works with --print, which would mean giving up the interactive terminal.

/compact, /clear, /model and /rewind are dispatched inside the TUI, and nothing outside the process can trigger a built-in command. Claude can invoke a skill, because that is a tool call. An interrupt lands at the next tool call, so a pure-text reply cannot be caught.

Claude Code documents where it stores a transcript but treats the contents as internal and free to change between releases, so on a version finestra has not seen you get a quietly reduced window rather than an error.

Claude Code is the only harness supported today. Everything that knows its file layout lives in src/harness/, so a second one is an adapter rather than a rewrite.

What finestra will not do

No hosted mode, no multi-user, no cloud relay, no account, no MCP server. It handles no credential and makes no network request. A human sends every message.

These are design decisions rather than a roadmap. The window is worth having because it runs entirely on one machine, and each of these would cost that.

License

MIT. See LICENSE.

finestra is not affiliated with, endorsed by, or sponsored by Anthropic. It reads files that Claude Code writes on your own machine, and Claude Code itself remains Anthropic's software under Anthropic's terms.

About

Claude draws its own interface, live, on your machine. It writes HTML panels as files, the page morphs them in place without reloading, and you click any element to steer. Local-only, zero dependencies.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages