Tithon for VSCode
Your Jupyter kernel dies with your client. It shouldn't.

Open a .pynb Python Notebook (or optional .py cell view) against a persistent remote kernel.
Close your editor, lose your connection, or return later; the same session is
still there and still streaming.
This is the VSCode client for Tithon. The kernel and all session state
live in a host-resident daemon, not in the editor, so disconnecting only stops
the rendering — never the work.
Status: alpha. It works and it's in daily use, but you will hit rough
edges. Bug reports are genuinely useful — please open an issue.
The problem
You SSH into a GPU box, start a long run in a notebook, and close your laptop.
When you come back, depending on your setup:
- VSCode Jupyter ties the kernel to the extension-host process. Close the
window or drop the network and the kernel dies, taking the session with it.
- JupyterLab reconnects, but everything printed while you were away is gone.
iopub output is streamed over the WebSocket and never persisted server-side,
so there is nothing to replay.
tmux + jupyter console survives the disconnect, but you lose rich
output (plots, HTML, widgets) and you can't open the same session from a
second client.
The root cause is the same in all three: the source of truth for your session
lives on the client, or in a channel that doesn't outlive a disconnect. Tithon
moves it to the host — the extension is a thin view over a daemon, not the owner
of the kernel.
Tip. And in the era of AI agents, one more: a .ipynb is JSON bloat — the
same notebook is ~250 lines of "cell_type"/"outputs" noise that Tithon
keeps as ~50 lines of clean .py. Output images? Tithon keeps them as real
files, so an agent hands them to the model as actual images it can see — not
base64 the model burns thousands of tokens on and still can't read.
Don't feed your AI idiot JSON.
Requirements
- The Tithon daemon on the host, which needs Python 3.11+
(
pip install tithon, or uv add tithon). The extension can start it for you
(see tithon.autoStartDaemon).
- A Unix-like host — the daemon uses unix domain sockets and
setsid (developed
and tested on Linux).
- VSCode 1.85+.
On Windows, we recommend running Tithon inside WSL and connecting with VS Code using the WSL extension.
Native Windows is not currently supported.
The intended setup is a Tunnel or Remote-SSH connection, where the
extension host runs on the remote host and reaches the daemon's local socket
directly — no port forwarding. It works exactly the same locally.
Install
code --install-extension rnoro.tithon
or search for "tithon" in the Extensions view. The daemon side ships on PyPI:
pip install tithon # or: uv add tithon
Quickstart
A Python Notebook (.pynb) opens directly in the notebook editor. Its disk
format is readable percent-format Python (# %% cells); outputs and metadata
live separately under .tithon/, and execution lives in the persistent daemon.
- Run Tithon: New Python Notebook… to create a saved
.pynb, or open one.
- Pick the Tithon kernel and run cells as usual.
Existing .py scripts still open as text by default. Open as Notebook keeps
the optional cell view; Copy as Python Notebook… creates a separate .pynb
with the saved source, current outputs and notebook metadata. Copying saves dirty
source first, refuses running/queued work and existing destinations, and stays
inside one project. It preserves the original file and creates a new URI/session:
live variables, imported modules and full execution history do not migrate.
Images are copied independently so rerunning one file cannot delete the other's
saved images. If the source has saved local session history, open it first so the
daemon can restore its authoritative outputs; wait for any unfinished work to recover before
copying. A deliberately terminated source copies no retired outputs or widgets.
Import Jupyter Notebook… defaults to .pynb; Export to Jupyter Notebook…
accepts .pynb and existing .py. Export Python Script… makes a separate
percent .py copy with saved outputs/metadata. Existing output/session history at
a destination also prevents copying, even if its source file was deleted.
That's all. Selecting the kernel attaches the session and restores any earlier
output automatically; new output then streams into the cells live as it is
produced. Close the laptop or drop the connection — the daemon and kernel keep
running. Reopen the notebook later (VSCode remembers the kernel) and the outputs
are back and resume streaming, with no command to run.
Outputs are matched to cells in three tiers — recorded cell index + content
hash first, then content hash across moved cells, and finally recorded cell
index when a cell was edited in place (flagged stale until re-run) — so outputs
survive cell insertions, moves, edits, and reopens. The source file itself
stays pure source — outputs never touch the file, so diffs stay clean.
What you get
- Python Notebook for percent
.pynb files and optional .py views, with per-cell run and a kernel
toolbar (interrupt / restart / select interpreter).
- Automatic restore + live sync — selecting the kernel (or reopening the
file) rebuilds the folded output state into the cells and resumes streaming;
stdout/stderr,
matplotlib figures, and rich display_data arrive in real
time, with no manual command.
- ipywidgets — standard controls, anywidget 0.11.x and some installed classic
AMD widgets with matching metadata and version exports, including certain locally
mapped dependencies, render through the widget manager; bqplot 0.13.1 and
pythreejs 2.4.2 are verified. Custom JavaScript requires a trusted workspace and
a live kernel; saved or untrusted custom models fall back to text. Reconnection
restores synchronized traits without replaying clicks. Browser-only state and
transient custom messages are not generally recoverable. CDN loading, broader
dependency discovery, Lab-only bundles and dedicated
Output layout remain in progress.
Some mixed saved or untrusted containers show supported labels and progress
bars in a clearly marked read-only snapshot with omissions indicated.
- Shareable outputs — the folded output state is kept as text in
<project>/.tithon/ (images referenced as deduplicated files, not embedded).
Commit it and whoever clones the repository opens the file with your results
already in it; leave it uncommitted to keep them to yourself.
Commands
Restore and live sync happen automatically, so there are no commands for them.
The rest are run from the command palette or the notebook's kernel toolbar:
| Command |
What it does |
Tithon: New Python Notebook… |
Create a .pynb file that opens directly as a notebook. |
Tithon: Copy as Python Notebook… |
Copy saved .py source and current outputs to an independent .pynb. |
Tithon: Export Python Script… |
Copy .pynb source and outputs to an independent .py. |
Tithon: Import Jupyter Notebook… |
Import a Python nbformat 4 .ipynb into a .pynb (or .py) with saved outputs and metadata. |
Tithon: Export to Jupyter Notebook… |
Export a .pynb or .py and its current/saved outputs to a .ipynb without running code. |
Tithon: Open as Notebook / Open as Text Editor |
Toggle a .py between notebook and plain text, once. |
Tithon: Always Open With… |
Choose Text Editor or Notebook for this one file, permanently in this workspace. |
Tithon: Restart Kernel |
Restart the session's kernel. |
Tithon: Interrupt Kernel |
Interrupt the running cell. |
Tithon: Select Python Interpreter |
Choose the interpreter the daemon runs the kernel as. |
Tithon: Restart Daemon |
Restart the host daemon. |
Tithon: Terminate Kernel… |
Kill the session's kernel. Reopening the file then leaves the cells empty — terminating means you are done with that session. |
Tithon: Restore Previous Outputs |
Put a terminated session's outputs back into the cells. |
Tithon: Retry Output Restore |
Retry connecting and restoring outputs for the active notebook after a failure. |
Tithon: Show Session Storage |
Show the active TITHON_HOME, retention policy, and per-session SQLite/image storage usage. |
Run Cell is offered as a CodeLens above each # %% cell.
A .py opens as a normal text editor unless you say otherwise. Use Tithon: Always Open With… from the Explorer context menu, editor title, or Notebook
toolbar to choose Text Editor or Notebook. Tithon records the choice as one
workbench.editorAssociations entry in the workspace's .vscode/settings.json,
so it survives reloads and can be committed for the team. If you want every
.py in a workspace to open as a notebook, that is VSCode's own setting rather
than a Tithon command — Reopen With… → Configure default editor… on a .py,
choosing Notebook - Tithon. It writes "*.py": "tithon-py", which applies to
every Python file in scope, setup.py and conftest.py included.
Settings
| Setting |
Default |
Purpose |
tithon.autoStartDaemon |
true |
Start the daemon on the host when it isn't already running (spawned detached, so it survives reconnects). |
tithon.daemonCommand |
tithon |
Command used to launch the daemon (run as <command> daemon). Set an absolute path if tithon isn't on PATH. |
tithon.pythonPath |
"" |
Run the daemon as <python> -m tithon daemon. Leave empty to use the Python extension's selected interpreter. |
tithon.homePath |
"" |
Directory for the daemon socket, log, kernel files, and journals (~/ supported). Empty uses TITHON_HOME, then ~/.tithon. Changing this requires Reload Window. |
tithon.reconnectMaxAttempts |
0 |
Maximum automatic reconnect attempts after losing the daemon connection (0 = unlimited). |
tithon.confirmDestructiveActions |
true |
Ask in a modal dialog before Restart Kernel, Terminate Kernel and Restart Daemon. Set to false if you restart often and accept losing the namespace to a mis-click. |
tithon.kernelIdleTimeout |
0 |
Reap a kernel after this many seconds idle (no attached notebook, nothing running or queued). Outputs stay restorable from the journal; only the in-memory variables are lost. 0 = never. Applied when the extension (re)starts the daemon. |
tithon.historyRetentionDays |
0 |
Delete raw output-history messages older than this many days while preserving current cell outputs, widgets, and lifecycle events (0 = keep all). Applied when the extension (re)starts the daemon. |
tithon.historyTargetMiB |
0 |
Soft per-session limit in MiB for raw output-history messages and referenced images (0 = keep all). Applied when the extension (re)starts the daemon. |
How it works
A long-lived daemon on the host owns the kernel and serves clients:

- The kernel runs detached (
setsid), so it is not a child of the daemon.
The daemon can crash, restart, or be upgraded; the kernel keeps running and
re-attaches through a persisted connection file.
- Every execution iopub message (plus daemon lifecycle and widget comm events) is
journaled to append-only SQLite (WAL) by default, alongside a per-execution
folded snapshot — the current display state — so reconnects are fast.
- The extension attaches with the last sequence number it saw and gets a
snapshot plus an ordered, gapless delta stream; reconnecting is just resuming
the stream.
- Rich outputs (images) are stored as files and referenced by hash, never
base64-embedded, and
ipywidgets traffic is folded into a widget-state+json
snapshot, so a tqdm bar or a slider comes back at its real value.
- Backpressure is bounded: per-subscriber buffers are capped, and a client that
falls too far behind is dropped and resyncs on reconnect — so one slow client
can't grow daemon memory or block the others.
- The daemon binds a
0600 unix domain socket. No TCP.
The kernel itself is plain ipykernel — Tithon replaces the session-management
layer around it, not the execution engine.
How it pairs with the daemon
The extension and the daemon must run on the same host and share
TITHON_HOME (both default to ~/.tithon for the same user). A
Tunnel/Remote-SSH session satisfies this for free: the extension host runs on
the host, so it uses the host-local socket and needs no forwarding. If you
instead run the extension on your laptop against a remote daemon, you must
forward the unix socket yourself (SSH RemoteForward, socat, …) — that is not
the default path.
The daemon also ships a CLI (tithon run, tithon attach, tithon status) for
driving the same sessions from a terminal. See the main project README
and the design spec for the full architecture.
Python language services
Notebook cells use the Python language. Pylance same/cross-cell definitions need
Tithon's guarded pseudo-path redirect for both suffixes. Ruff's default file
selection excludes .pynb; add this explicitly to your project's ruff.toml
(or the equivalent tool.ruff setting in pyproject.toml):
extend-include = ["*.pynb"]
Tithon never rewrites your Ruff configuration. Real VS Code probes verified
Pylance completion, imported-file definitions and inlay providers; ty diagnostics,
cell/imported definitions and inlays; and Ruff diagnostics/edit/reopen with the
setting above. Full language compatibility is not established: Pylance diagnostics
and the targeted ty str.upper completion did not pass matched .py controls.
The usual text/notebook lifecycle guards remain necessary when manually reopening
source as text; routine text-toggle controls are shown only for .py.
License
MIT.