Documentation
¶
Overview ¶
Package render formats the prosa timeline for both interactive terminals (Lipgloss colors, day-grouped headers, active markers) and non-interactive sinks (pipes/redirects/scripts — plain tab-separated rows without escape codes).
Index ¶
- Constants
- Variables
- func Analytics(w io.Writer, r store.AnalyticsResult, interactive bool) error
- func CleanFirstPrompt(s string) (string, bool)
- func ContextLine(opts ContextLineOptions) string
- func DayHeader(t, now time.Time) string
- func DeviceLabel(m map[string]string, id string) string
- func IsActive(lastActivityAt, now time.Time) bool
- func RenderFirstPrompt(s string) string
- func SearchContextLine(opts ContextLineOptions) string
- func SearchHits(w io.Writer, hits []store.SearchHit, now time.Time, interactive bool) error
- func SearchHitsWithOptions(w io.Writer, hits []store.SearchHit, now time.Time, opts SearchOptions) error
- func ShowSession(w io.Writer, d SessionDetail) error
- func Timeline(w io.Writer, sessions []session.Session, now time.Time, interactive bool) error
- func TimelineItems(w io.Writer, items []TimelineItem, now time.Time, opts TimelineOptions) error
- type ContextLineOptions
- type ContextScope
- type RowSlots
- type SearchOptions
- type SessionDetail
- type TimelineItem
- type TimelineLayout
- type TimelineOptions
Constants ¶
const ( ColorMuted = lipgloss.Color("#8A8F98") ColorRail = lipgloss.Color("#3A3F46") ColorAccent = lipgloss.Color("#8AB4D6") ColorDevice = lipgloss.Color("#7FB3C8") ColorAgent = lipgloss.Color("#D6B97A") ColorProject = lipgloss.Color("#8CBF88") ColorActive = lipgloss.Color("#D7827E") ColorError = lipgloss.Color("#D7827E") )
Semantic palette from docs/cli/rendering-contract.md. Hex values are resolved in truecolor when the terminal supports it; Lipgloss falls back to the nearest 256-color index automatically when it doesn't.
const ActiveThreshold = 10 * time.Minute
ActiveThreshold is the cutoff for considering a session "live" per INTENT.md §4. Fixed at 10 minutes in the MVP — no per-agent or per-user knob until lived experience says otherwise.
const MetaPlaceholder = "(meta)"
MetaPlaceholder is the muted token rendered in place of a boilerplate first_prompt so the column is never empty but the reader instantly knows the absence of real user content is intentional.
Variables ¶
var ( StyleMuted = lipgloss.NewStyle().Foreground(ColorMuted) StyleRail = lipgloss.NewStyle().Foreground(ColorRail) StyleAccent = lipgloss.NewStyle().Foreground(ColorAccent) StyleDevice = lipgloss.NewStyle().Foreground(ColorDevice) StyleAgent = lipgloss.NewStyle().Foreground(ColorAgent) StyleProject = lipgloss.NewStyle().Foreground(ColorProject) StyleActive = lipgloss.NewStyle().Foreground(ColorActive).Bold(true) StyleMatch = lipgloss.NewStyle().Foreground(ColorAgent).Underline(true) StyleSuccess = lipgloss.NewStyle().Foreground(ColorProject) StyleSkipped = lipgloss.NewStyle().Foreground(ColorMuted) StyleWarning = lipgloss.NewStyle().Foreground(ColorAgent) StyleError = lipgloss.NewStyle().Foreground(ColorError) StyleHeader = lipgloss.NewStyle().Bold(true) )
Functions ¶
func Analytics ¶
Analytics writes the result as a fixed-width table. In TTY mode the header is bold/dim, numeric columns use a soft accent; out of TTY the same data is tab-separated with no styling so shell pipelines parse it trivially.
func CleanFirstPrompt ¶
CleanFirstPrompt classifies the raw first_prompt. Returns the best-effort human portion (stripping known wrapper blocks when one surrounds a real prompt) and a boolean reporting whether the result is real user content. The renderer uses (_, false) → MetaPlaceholder.
Patterns and wrapper logic live in internal/sessiontext so importers, the SQL denoise mirror, and this renderer all agree.
func ContextLine ¶
func ContextLine(opts ContextLineOptions) string
ContextLine builds the stderr context anchor printed before timeline or analytics output. Examples:
prosa · local · scoped to prosa · last 7d prosa · local · all projects · last 7d prosa · local · project not detected · showing all projects · last 7d prosa · remote · scoped to prosa · last 30d
func DayHeader ¶
DayHeader returns the relative label INTENT.md §8 specifies for the timeline grouping header.
0 days ago: "Today" 1 day ago: "Yesterday" 2..6 days ago: "N days ago" 7..30 days ago: weekday name (e.g. "Wednesday") more than 30 days: absolute date "Jan 02"
The comparison is done on day boundaries in t's timezone, not via straight Sub(), so a session at 23:55 yesterday and one at 00:05 today land under separate headers as users expect.
func DeviceLabel ¶
DeviceLabel translates a device fingerprint into the human label used in the timeline / search rows. When the caller has a populated map (id → friendly_name), the mapped value wins. Otherwise we fall back to the first 7 hex chars + "…", which at least signals the shape of the id while staying narrow enough for the row layout.
The fallback should never be hit in practice — `prosa sync` upserts a device row before any session writes — but we keep it defensive for sessions loaded from external stores.
func IsActive ¶
IsActive returns true when the most recent activity in the session happened within the threshold window. Centralized so the rule lives in exactly one place.
func RenderFirstPrompt ¶
RenderFirstPrompt returns the styled first prompt, or MetaPlaceholder (muted) when the content is boilerplate.
func SearchContextLine ¶
func SearchContextLine(opts ContextLineOptions) string
SearchContextLine is the variant used by `prosa search`. It carries the query in quotes instead of the time window.
search · local · scoped to prosa · "sqlite"
func SearchHits ¶
SearchHits prints one block per hit: a session header line followed by a snippet sub-line with the matched terms highlighted.
func SearchHitsWithOptions ¶
func ShowSession ¶
func ShowSession(w io.Writer, d SessionDetail) error
func Timeline ¶
Timeline writes sessions to w grouped by day with Lipgloss colors when interactive == true, or as plain tab-separated lines otherwise.
func TimelineItems ¶
func TimelineItems(w io.Writer, items []TimelineItem, now time.Time, opts TimelineOptions) error
Types ¶
type ContextLineOptions ¶
type ContextLineOptions struct {
Command string
Source string
Scope ContextScope
ScopeLabel string
Last string
Since string
Between string
Query string
}
ContextLineOptions is the input to ContextLine and SearchContextLine. Command is "prosa" or "search" (callers fill it). Source is "local" or "remote". Exactly one of Last / Since / Between is non-empty at call time: Last is the rolling-window token ("7d", "12h"); Since is a YYYY-MM-DD anchor; Between is a pre-formatted "YYYY-MM-DD and YYYY-MM-DD" range string. The renderer adds the appropriate "last " / "since " / "between " prefix. Query is only used by the search variant; ignored by ContextLine.
type ContextScope ¶
type ContextScope int
ContextScope names the project-scoping state shown in the context line. Helpers below build the textual segment per state so call sites don't reimplement the formatting.
const ( // ScopeScoped means a project was detected (auto from cwd, or set // explicitly via --project). Label carries the project name. ScopeScoped ContextScope = iota // ScopeAll means the caller passed --all, opting out of scoping. ScopeAll // ScopeProjectNotDetected means scoping was attempted but the cwd // is not part of any known project. Output falls back to all // projects with a discoverable hint. ScopeProjectNotDetected )
type RowSlots ¶
RowSlots decides which optional columns the timeline (or search hit) renders. A slot set to false collapses the column entirely so the remaining ones absorb the freed width. This is the cardinality-1 rule: when every row in the set shares the same value for a column, render that column once in the context line and drop it from the rows.
func ResolveSlots ¶
func ResolveSlots(items []TimelineItem, layout TimelineLayout) RowSlots
ResolveSlots inspects items + layout intent and returns the slot flags. Rules:
- TimelineScoped implies omit-project regardless of cardinality (the context line already names the project).
- 1 distinct device id → omit device.
- 1 distinct project label → omit project.
Agent is never a slot — it's central enough that we always render it, even when uniform.
type SearchOptions ¶
type SearchOptions struct {
Interactive bool
Width int
// DeviceLabels maps device_id → friendly_name so search hits show
// "Studio M4" instead of the raw fingerprint hex.
DeviceLabels map[string]string
// HideProject drops the project segment from the meta line. The
// timeline/search context line already names the project when the
// caller is scoped.
HideProject bool
// HideDevice drops the device segment from the meta line — used
// when every hit shares the same device (cardinality 1).
HideDevice bool
}
type SessionDetail ¶
type TimelineLayout ¶
type TimelineLayout int
const ( TimelineScoped TimelineLayout = iota TimelineGlobal )
type TimelineOptions ¶
type TimelineOptions struct {
Interactive bool
Width int
Layout TimelineLayout
// Slots controls scope-aware column suppression. Callers must set
// this explicitly (via ResolveSlots); leaving it zero means
// "render no device, no project" — which is intentional but rare,
// so the convenience wrapper Timeline() always passes both true
// for backward compatibility.
Slots RowSlots
// DeviceLabels maps device_id → friendly_name; the row uses it via
// DeviceLabel(...) so the column shows "Studio M4" instead of the
// raw hex fingerprint. Nil is OK — the fallback handles it.
DeviceLabels map[string]string
}