render

package
v0.27.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: CC0-1.0 Imports: 14 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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

View Source
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 AbbreviateHome added in v0.27.4

func AbbreviateHome(p string) string

AbbreviateHome rewrites an absolute path under $HOME to the ~ form.

func Analytics

func Analytics(w io.Writer, r store.AnalyticsResult, interactive bool) error

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

func CleanFirstPrompt(s string) (string, bool)

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
prosa · local · all projects · last 1d · project c3-oss/prosa · device tbox

func DayHeader

func DayHeader(t, now time.Time) string

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

func DeviceLabel(m map[string]string, id string) string

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 — trimmed to its first dot segment so FQDN-style hostnames ("ip-10-0-0-1.ec2.internal") read like `hostname -s`. 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

func IsActive(lastActivityAt, now time.Time) bool

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 NormalizeRemote added in v0.27.4

func NormalizeRemote(remote string) string

NormalizeRemote collapses a git remote URL down to "owner/repo" form. Anything that does not look like a URL is returned as-is so callers preserve best-effort project information instead of dropping data.

func RenderFirstPrompt

func RenderFirstPrompt(s string) string

RenderFirstPrompt returns the styled first prompt, or MetaPlaceholder (muted) when the content is boilerplate.

func ResolveSlots

func ResolveSlots(items []TimelineItem, layout TimelineLayout) (RowSlots, UniformValues)

ResolveSlots inspects items + layout intent and returns the slot flags plus the uniform values behind any suppressed column. 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.

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

func SearchHits(w io.Writer, hits []store.SearchHit, now time.Time, interactive bool) error

SearchHits prints one block per hit: a session header line followed by a snippet sub-line with the matched terms highlighted.

func SearchHitsWithOptions

func SearchHitsWithOptions(w io.Writer, hits []store.SearchHit, now time.Time, opts SearchOptions) error

func ShowSession

func ShowSession(w io.Writer, d SessionDetail) error

func Table added in v0.27.4

func Table(w io.Writer, cols []TableColumn, rows [][]TableCell, interactive bool) error

Table writes a fixed-width table: bold muted headers and display-width-aligned columns in interactive mode, tab-separated rows otherwise so shell pipelines keep a stable machine-friendly shape.

func Timeline

func Timeline(w io.Writer, sessions []session.Session, now time.Time, interactive bool) error

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
	UniformProject string
	UniformDevice  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. UniformProject / UniformDevice carry values dropped from the rows by cardinality-1 suppression; they render as trailing segments so the information survives the collapsed column.

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

type RowSlots struct {
	Device  bool
	Project bool
}

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.

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 SessionDetail struct {
	Session session.Session
	Tools   []session.ToolUsage
	Turns   []session.Turn
	Width   int
	// MaxOutputLines caps the number of lines printed per turn. 0 means
	// no cap; negative values use the legacy single-line collapse.
	MaxOutputLines int
	// DeviceLabels maps device_id → friendly_name for the header line.
	DeviceLabels map[string]string
}

type TableCell added in v0.27.4

type TableCell struct {
	Text  string
	Style lipgloss.Style
}

TableCell is one rendered value. Style applies after alignment so padding is computed on the visible text, not on escape sequences.

func Cell added in v0.27.4

func Cell(s string) TableCell

Cell wraps plain text in an unstyled TableCell.

type TableColumn added in v0.27.4

type TableColumn struct {
	Header string
	Right  bool
}

TableColumn describes one column of a dense CLI table. Right-aligned columns hold numbers so magnitudes line up for rapid comparison.

type TimelineItem

type TimelineItem struct {
	Session session.Session
	Tools   []session.ToolUsage
}

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
}

type UniformValues added in v0.27.4

type UniformValues struct {
	DeviceID string // single device id, when only one distinct device
	Project  string // single project label, when only one distinct project
}

UniformValues carries the value shared by every row when a column collapses to cardinality 1, so the caller can surface it in the context line instead of dropping the information entirely.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL