Windsurf
← all harnesses · parser: crates/core/src/session/windsurf.rs
The least legible of the eight, and the only page here with a standing caveat on its own accuracy. Windsurf is a VS Code fork, so its conversations live inside the editor’s per-workspace settings database, and its accounting lives on Codeium’s servers where nothing local can see it.
Where it lives
$WINDSURF_USER_DIR/workspaceStorage/<hash>/state.vscdb. Without the override,
the User directory follows the platform: ~/Library/Application Support/Windsurf/User on macOS, %APPDATA%\Windsurf\User on Windows, and the
XDG config directory elsewhere. $WINDSURF_USER_DIR names the whole User
directory rather than a parent, which is what a portable install actually moves.
The working directory comes from workspace.json beside the database, where VS
Code records it as a file:// URI; it is percent-decoded by hand, because
undoing %XX is the entire job and a URI crate would be a dependency for six
lines. A remote or virtual workspace has no local path and keeps an empty label.
The format
SQLite, opened read-only. ItemTable maps a settings key to a JSON blob, and
Cascade parks its conversations in one of those blobs as a list of tabs, each
holding the bubbles of one conversation. One row per tab: tabId for the id
(string or number, both accepted), chatTitle for the title.
The stored value is taken as raw bytes, Text or Blob alike: VS Code writes
some ItemTable entries as one and some as the other, and asking rusqlite for a
concrete type fails outright on the wrong one.
The caveat
ponytail: the parser is written against the documented
ItemTablelayout rather than a Windsurf install, so the settings-key list and the bubble field names are the part that could be wrong.
Four keys are tried in order — cascade.chatdata,
workbench.panel.aichat.view.aichat.chatdata, aiChat.chatdata, chat.data —
and the first that parses into tabs wins. Tool calls are read from either
toolCalls or tool_calls, with a name under either name or toolName.
Both fail closed: an unrecognised key yields no rows, an unrecognised bubble
yields no tool calls. So a wrong guess costs visibility and never produces a
wrong number. If you can read the real key off a live install, add it to
CHAT_DATA_KEYS. Until then, the README’s ✓ in Windsurf’s Tools column is
provisional in a way the other seven are not.
What cannot be extracted
No model, no tokens, no cost, no per-message timestamp, no tool arguments, no
per-call outcome. Windsurf bills in credits server-side and keeps none of it
locally, so list_sessions sets cost_available = false and total_cost = None: these sessions report cost as unavailable rather than as free. Tool
details carry the tool’s name and nothing else, because a fabricated argument
line would read as though the blob had recorded one.
Both timestamps are the database’s mtime. Windsurf stamps neither a start nor an end on a conversation, so every tab in a workspace shares the file’s time. That is honest about ordering — the most recently used workspace really did change last — and deliberately says nothing else.
The consequence reaches the cache: cache::disk_key returns None for
Windsurf, because there is no per-conversation stamp to build an honest key
from, so its rows are never persisted and re-extract together whenever the
workspace file moves. That is cheap, since the blob is small.
What cctop will not do
delete returns Unsupported with a message pointing at the editor. Removing
one conversation means rewriting a settings blob inside a live editor’s own
database, which is not a thing to do behind a running Windsurf — and reporting
the refusal beats returning success and leaving the row on screen.
There is no resume command, no process to match (Windsurf’s agent is the editor
itself), and no hooks: access.rs lists a project’s .windsurfrules and notes
that the global rules live in the settings UI, where nothing can read them.