History import
Picasso can bring in past sessions from Claude Code, Codex and OpenCode as read-only threads. The code is packages/sessions/src/history-import* (@picasso/sessions/history-import), shared by the CLI, the TUI and the Mac helper.
Where imported threads are stored, and why
Imported threads are written as engine session logs, not as Picasso's own local session journal (~/.picasso/sessions/*.jsonl):
- The Mac thread list comes from the engine's
session/list(relayGET /threads). It does not read the local journal. - The TUI resume list (
LocalSessionStore.listSessionIndex) already lists engine-only sessions, the same way it shows
threads the Mac started.
The engine log is the only format both surfaces already read, so an imported thread shows in both with no new reader.
Each import is <DSH_HOME>/sessions/<project-key(cwd)>/<id>/session.v4.jsonl.zstd (<DSH_HOME> is the engine home, ~/.picasso/dsh-home by default), written the way the engine writes logs: a header frame followed by a body frame, each one a checksummed zstd frame. The header holds only the engine's header keys. The id is a UUID-shaped hash of source:sourceId, so it is the same on every run. The log uses stock event types only:
session/title, pinned with theusersource so a later auto-title does not replace it,- per turn:
turn/start,user/message,step/start,assistant/message,step/end,turn/end, - one ignorable
plugin:picasso/history-importrecord holding the source, path and content hash.
Tool calls are summarised as a list at the end of the turn's assistant text (Tool calls (n): - \Bash\ pnpm test). No tool/call records are written, so continuing a thread on another model never replays another tool's calls. Reasoning and tool output are not imported. The record shapes were checked against the pinned engine's adoptSessionEvent, validateStoredEvents and format-catalog header reader. A test runs adoptSessionEvent when the package is installed.
Sources (read-only)
| Source | Location | Notes | ||
|---|---|---|---|---|
| Claude Code | $CLAUDE_CONFIG_DIR/projects/*/*.jsonl (default ~/.claude/projects) | Subagent transcripts, sidechain and meta records are skipped. The title is custom-title, else ai-title, else summary, else the first prompt. | ||
| Codex | $CODEX_HOME/sessions/**/rollout-*.jsonl (default ~/.codex/sessions) | Prompts come from event_msg:user_message, or from user response_items minus injected instructions in older rollouts. Thread names come from session_index.jsonl. Subagent threads (parent_thread_id) are skipped. | ||
| OpenCode | $XDG_DATA_HOME/opencode, ~/.local/share/opencode, or ~/Library/Application Support/opencode | Reads opencode.db (session_v2 + session_message, then the older session + message + part) opened read-only, plus the legacy JSON `storage/session | message | part`. Only top-level sessions are imported. |
Idempotency
<picassoHome>/imports/history.json records one entry per imported session: source, path, fingerprint (size+mtime, or the row's updated time), content hash, and the mtime of the log that was written. On a later run:
- a matching fingerprint is skipped without parsing,
- a matching hash is skipped,
- a changed source is rewritten, unless the imported log changed after it was written (it was opened or continued),
in which case it is left alone.
A session with no typed prompt is counted as "no prompts" and is not imported.
Read-only until continued
The relay adds imported: {source, label, importedAt} to GET /threads rows, and the TUI resume list shows an "Imported" badge. Both read the manifest.
- Mac: an imported thread has no composer. Continue forks it with
POST /threads/:id/forkand opens the fork. - TUI: the first prompt sent into an imported thread forks it first (
continueImportedSession): a new session id
gets the same turns without the import mark. The prompt runs there, and the imported thread stays as it was.
Consent and surfaces
Nothing is imported without one click or keypress of consent. The answer is stored in the shared settings as settings.historyImport = {decision, sources, decidedAt} (imported, skipped, or later = offered again on a launch a day or more after). The last picker selection is kept in <picassoHome>/imports/selection.json.
- Picker (all surfaces): one row per source found (checkbox, name, sessions, date range, projects). Customise
expands a source: projects (most recent first, select all/none), a date filter (all, 30 days, 90 days, last year) and "skip very short sessions (≤ 1 prompt)". The footer shows live totals. Counts come from scanHistory (directory listings, mtimes, Codex's first line, OpenCode rows); scanHistoryDetails then reads the first 256 KB of each file for prompt counts and Claude Code's real project paths, cached per fingerprint in <picassoHome>/imports/scan-cache.json.
- Live conversion view: per-source bars and counters (converted, skipped: already imported / no prompt / short,
failed with the reason), the session being converted, an ETA, then "Imported N · skipped N · failed N — open with /resume". Failures can be retried (only those source paths are read again). The importer emits start, session, converted and done events; runImportJob folds them into the status (perSource, current, failures).
- TUI: on the first signed-in launch the picker is the first screen (never stdin prompts);
/import(or
/import sessions) opens it any time; /import skills and /import <path> keep the skill import. The run is in-process; esc hides it into a header chip. No TTY, --json or -p: never prompts.
- CLI:
picasso importwith no arguments on a terminal opens the same picker (esc while it runs hands the rest to a
background picasso import --selection <json>); after picasso auth login it opens when due. picasso import [claude|codex|opencode|all...] [--dry-run] [--json] imports now; picasso import --status shows the last or running import (<picassoHome>/imports/status.json).
- Mac: onboarding and Settings › Import show the same picker and live view.
- Relay routes:
GET /desktop/import/scan(fast),GET /desktop/import/details, `POST /desktop/import/run
{sources, filters?}, POST /desktop/import/retry, POST /desktop/import/skip, POST /desktop/import/later, GET /desktop/import/status (live while the relay runs the import), and the older GET /desktop/import/sources`.