render

package
v0.27.2 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: CC0-1.0 Imports: 10 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 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

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. 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 RenderFirstPrompt

func RenderFirstPrompt(s string) string

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

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 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
}

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

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.

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 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
}

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
}

Jump to

Keyboard shortcuts

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