Documentation
¶
Overview ¶
Package panehistory stores and serves per-pane user-input history. One JSONL file per pane lives under <quilDir>/history/<paneID>.jsonl. The Claude hook subprocess appends entries; the daemon reads, previews, and compacts.
Concurrency: the hook subprocess only ever O_APPENDs single lines and the daemon only reads/compacts, so writes never interleave in practice. On Linux an O_APPEND write of one line is atomic; on Windows the guarantee is weaker for very large lines, but Read tolerates a malformed/partial trailing line and Compact's rename is best-effort (a transient Windows sharing-violation is logged by the caller and retried on the next read). Treat "concurrent" as serial-enough, not lock-free-safe.
Index ¶
Constants ¶
const ( // MaxEntryBytes caps a single entry's text — generous enough for a pasted // stack trace, bounded so one paste can't bloat the store. MaxEntryBytes = 64 * 1024 // MaxEntries is the ring cap Compact enforces. MaxEntries = 200 )
Variables ¶
This section is empty.
Functions ¶
func Append ¶
Append writes one entry to the pane's history file. Empty/whitespace text and synthetic harness-injected turns (see IsSyntheticPrompt) are skipped (returns nil without writing). Oversize text is truncated on a rune boundary with a trailing marker. V is forced to the current schema version. O_APPEND keeps concurrent hook invocations from clobbering each other.
func Compact ¶
Compact rewrites the pane's history file keeping only the last keepLast real entries, dropping synthetic harness-injected turns (see IsSyntheticPrompt) in the process. It rewrites ONLY when the number of REAL entries exceeds keepLast. When real entries are within the cap the file is left untouched — even if pre-existing synthetic junk is on disk — because Read-time filtering already hides that junk from every caller, and rewriting would race the Claude hook's cross-process O_APPEND (a prompt appended between readRaw and the rename would be written to the old inode and lost). Gating on the RAW line count instead would reopen that race: historical task-notifications can push a pane with few real prompts over the cap, triggering a needless rewrite. Since Append no longer records synthetic turns, that junk is a finite, non- growing residue; it is purged only as part of a genuine over-cap trim of real entries. Atomic via temp file + rename.
func IsSyntheticPrompt ¶ added in v1.34.2
IsSyntheticPrompt reports whether text is a harness-injected turn (not real user input) that must be excluded from history. A prompt is synthetic only when, after trimming surrounding whitespace, it BOTH opens with SOME known tag AND closes with SOME known end tag — i.e. it is machine-generated blocks and nothing else. Requiring both ends means a real prompt that merely quotes or discusses a tag ("<task-notification is confusing, what is it?") is preserved; only a pure, complete block is dropped.
The two ends are matched INDEPENDENTLY rather than as a pair, because one turn can carry blocks of different kinds: with parallel subagents, a <task-notification> and an <agent-message> arrive concatenated, and a pair-wise test matches neither (the first tag's open hits but not its close, the second's close hits but not its open) — letting exactly the noisiest machine turns through. No real prompt both starts with one of these tags and ends with one of them.
func PreviewLine ¶ added in v1.45.3
PreviewLine renders text as a SINGLE display line for the history list: every run of whitespace (newlines and tabs included) collapses to one space, and the result is truncated rune-aware to maxBytes with a trailing "…".
Flattening happens daemon-side rather than in the TUI because the list shows exactly one row per entry: a multi-line prompt has to become one line somewhere, and doing it before the wire keeps the payload proportional to what is actually displayed.
Control characters (category Cc — C0, DEL and C1) are DROPPED, not substituted. A prompt is free text the user may have pasted into, so it can carry an ANSI escape; an ESC that reached the list would repaint or reposition inside a fixed-width bordered modal that assumes one cell per printed column. Substituting a visible placeholder would keep the row honest about length but add noise to every row for a case that is always accidental.
Unicode format characters (category Cf) go too, which IsControl does NOT cover and IsSpace does not catch either. A bidi override or isolate (U+202A–U+202E, U+2066–U+2069) renders the row reversed while still measuring its pre-override width, and the user picks which prompt to open from exactly that row — so the row could read as something other than what Enter fetches. ZWSP is the quiet half: invisible, not IsSpace, and therefore able to defeat the whitespace collapse this function is built around. Same reasoning, same categories as claudesessions.SanitizeTitle, which sanitizes the sibling data class (Claude session titles) for the same non-VT render path.
Types ¶
type Entry ¶
type Entry struct {
V int `json:"v"`
TsMs int64 `json:"ts_ms"`
SessionID string `json:"session_id,omitempty"`
Text string `json:"text"`
}
Entry is one recorded user input, persisted as a single JSONL line. TsMs doubles as the entry's lookup id on the fetch-one-entry IPC path; two submissions in the same millisecond would collide, but human prompt cadence makes that effectively impossible (a collision just returns the first match).
func Read ¶
Read returns all real user entries oldest-first, excluding synthetic harness-injected turns (see IsSyntheticPrompt) so pre-existing junk written before the filter existed never surfaces. A missing file is not an error (returns nil, nil). Malformed lines — including a trailing partial line from an in-flight concurrent append — are skipped. A file larger than maxReadBytes is read from the tail so daemon memory stays bounded (the dropped first line is the oldest; Compact normally keeps the file well under the cap).