tui

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package tui is the terminal client.

It is one client among several, not the product: no session state lives here beyond scroll position, panel visibility and the input queue. Closing and reopening it replays the session from the log and lands on the same screen.

The design problem is not "how do I display a conversation". The agent produces ten to a hundred times more output than anyone will read, and most of it is not for reading — it is evidence that work is happening. Show everything and the user stops reading, missing the thing that mattered; show too little and they cannot trust what they cannot see.

The view model is a pure reducer over the event log, deliberately separated from rendering so both are testable without a terminal.

Spec: docs/specs/architecture/client-tui/202608081250-*.

Index

Constants

View Source
const Fallback = PtBR

Fallback is where an unknown or absent language lands.

pt-BR, and that is product identity rather than imposition: whoever has their machine in English gets English, and what the fallback decides is only which language wins when there is no information at all. A product whose fallback is pt-BR is a Brazilian product; one whose fallback is English is an English product with a translation bolted on.

View Source
const InitPrompt = `` /* 1640-byte string literal not displayed */

InitPrompt is what `/init` sends.

It is a turn, not a template: what belongs in DCODE.md depends on what the repository already says about itself, and only reading it can answer that.

View Source
const MaxImageBytes = 10 << 20

MaxImageBytes is the largest picture that goes to the model.

Ten megabytes, which is what the providers take. Refusing here rather than on the wire means the person hears "that file is too big" while they can still pick another one, instead of a rejected request after the turn started.

View Source
const MaxInputRows = 10

MaxInputRows is how tall the input box is allowed to get.

Ten, and the number is a compromise. One row made a list impossible to type, which is what this whole thing is about. No cap at all means a pasted essay takes the terminal and the person loses the conversation they were pasting it into.

View Source
const NameLimit = 120

NameLimit is what the daemon accepts, repeated here so the keyboard stops at the same place the server would refuse. A client that lets somebody type past the limit and then reports a failure has wasted the typing.

Variables

View Source
var Builtins = []Builtin{
	{Name: "help"}, {Name: "init"}, {Name: "clear"}, {Name: "plan"},
	{Name: "config"}, {Name: "model"}, {Name: "mode"}, {Name: "loop"}, {Name: "resume"},
	{Name: "undo"}, {Name: "image"}, {Name: "update"},
}

Builtins is the whole built-in set, in the order `/help` prints them. Builtins are the commands the client itself provides.

Name is the only field here now: the argument shape and the one-line help are text a PERSON reads, so they live in the catalogue with everything else the client composes. Keeping them here would have made /help the one screen that stays English.

View Source
var ErrNoClipboardTool = errors.New("no clipboard tool on this machine")

ErrNoClipboardTool means this machine has no way to read the clipboard at all. A different answer from an empty clipboard, and it needs a different response: install one, or use /image with a path.

View Source
var ErrNoImageInClipboard = errors.New("no image in the clipboard")

ErrNoImageInClipboard means the clipboard holds something, and it is not a picture. Distinct from a failure to look, because the two want different answers: one is "paste some text then", the other is "this machine has no way to read the clipboard".

Functions

func ActivityVerb added in v0.2.0

func ActivityVerb(tool string, frame int, l Lang) string

ActivityVerb returns the gerund to draw beside a running tool.

Empty for an empty tool, and that is the invariant rather than an edge case: with no fact to accompany, there is nothing for a verb to be true about.

func ActivityVerbsEnabled added in v0.2.0

func ActivityVerbsEnabled(env func(string) string) bool

ActivityVerbsEnabled reads the one setting this costs.

Default on. It is a client-side presentation choice and has nothing to do with `behavior.show_reasoning`: that one decides what the person is shown of the model's thinking, this one decides whether a line already on screen carries a word.

Read here and passed in as geometry, because internal/tui never reads the environment — the same arrangement as the palette and the language.

func AtBottom

func AtBottom(top, total, height int) bool

AtBottom reports whether the window is showing the end of the stream.

func BodyHeight

func BodyHeight(m Model, g Geometry) int

BodyHeight is how many rows the stream gets: everything but the status bar, the input line, and the working line when a turn is running.

func ClipboardImage

func ClipboardImage() ([]byte, string, error)

ClipboardImage returns the picture on the clipboard, as bytes.

The terminal cannot help here, and that is the whole difficulty. A paste arrives as bracketed text or as a key press; an image on the clipboard produces nothing either way. So dcode has to go and ask the operating system itself, which is why this is one file per platform shelling out to the tool that platform ships.

This runs in the CLIENT, not in the agent, so it is not behind the sandbox and does not need to be: the person pressed the key, and reading their own clipboard is the thing they asked for.

func ColorEnabled

func ColorEnabled(env func(string) string) bool

func ContextLabel

func ContextLabel(used, window int) string

ContextLabel renders the context meter.

A percentage alone disappears on a large window: five thousand tokens of a million is zero in integer division, so the meter a user most wants early in a long session is exactly the one that never shows. Below one percent it says so rather than rounding to nothing.

func CopyHint

func CopyHint(c CopyState, lang Lang) string

CopyHint is the line shown while copy mode is open.

It names every key, because a mode with no visible way out is a mode people force-quit the program to escape.

func CopyText

func CopyText(lines []string, c CopyState) string

CopyText joins the selected lines.

Undecorated: what goes to the clipboard is what the person meant to copy, not the cursor marks and box drawing around it. Pasting a diff with a gutter of ANSI escapes into an issue is the failure this avoids.

func FormatDuration

func FormatDuration(d time.Duration) string

FormatDuration renders a tool's elapsed time the way a reader scans it: milliseconds while it is fast enough not to notice, seconds once it is not.

func HelpText

func HelpText(user config.CommandSet, lang Lang) string

HelpText renders `/help`. Pure over the discovered command set.

func InputHeight added in v0.4.0

func InputHeight(m Model, g Geometry) int

InputHeight is every row the input area occupies, its frame included. The stream's height is computed from this, so a box that grew used to paint over the last lines of output.

func InputRows

func InputRows(m Model, g Geometry) int

InputRows is how many rows the box occupies.

The layout and the renderer both read this, and that is the point. BodyHeight used to subtract a literal 3 — status, input, bottom bar — with the input's share hard-coded at one. A box that grew without that number growing with it would paint over the stream, which is the ghosting already fixed once in "a painted frame owns every cell it covers". Two places computing a height is the bug; the symptom is only where it shows up.

func LineDown

func LineDown(text string, at int) int

LineDown is where the caret lands one row down, or -1 when there is none.

func LineEnd

func LineEnd(text string, at int) int

LineEnd is the offset just before the next break, or the end of the text.

func LineStart

func LineStart(text string, at int) int

LineStart is the offset of the beginning of the line the caret is on.

Home means this line, not the whole buffer. On one line the two are the same and nothing changes; on three they are the difference between correcting a word and jumping to the top.

func LineUp

func LineUp(text string, at int) int

LineUp is where the caret lands one row up, or -1 when there is no row above.

The -1 matters: with nothing above, up is not a movement at all, and the caller falls back to walking the command history — which is what up has always done on an empty line.

The column is kept where the shorter line allows and clamped to its end otherwise, because overshooting would land on a row the user did not aim at.

func LoopPlan added in v0.13.0

func LoopPlan(specs []protocol.SpecFolder, t Strings) string

LoopPlan is what a `/loop <goal>` says before it starts.

Every folder, not only the pending ones. A list that showed just the work left would leave someone unable to tell "this spec is finished" from "dcode did not see this spec", and those need different reactions.

func LoopTask added in v0.11.1

func LoopTask(spec LoopArgs) string

LoopTask is the turn `/loop` submits.

It submits one at all because loading a definition of done and then waiting is the command doing half its job: `/loop specs/x` means "do this", and it used to mean "open a session against this and sit there". Someone typed it, watched nothing happen, and had to say what they wanted anyway.

The criteria are NOT restated here. They are already the session's, the loop checks them, and a copy in the first message is a second statement of something that can move.

func MaxScroll

func MaxScroll(total, height int) int

MaxScroll is the furthest down the window can go.

func OSC52

func OSC52(s string) string

OSC52 is the escape sequence that puts text on the system clipboard.

It works over ssh and inside tmux, which is the whole reason to use it rather than shelling out to pbcopy or xclip: the terminal the person is looking at is not always on the machine dcode is running on, and a clipboard that only works locally is one that fails exactly when it is most wanted.

A terminal that does not support it ignores the sequence. That is a silent failure, so the client says what it did rather than assuming it worked.

func PageSize

func PageSize(m Model, g Geometry) int

PageSize is how far a page key moves: one screen less two rows of overlap, so the line you were reading is still there to anchor you.

func Pick

func Pick(ctx context.Context, choices []SessionChoice, geo Geometry, lang Lang) (string, error)

Pick asks which conversation to continue and returns its id, empty when the person chose none.

func PlanText

func PlanText(m Model) string

PlanText renders the full plan for `/plan` without an argument.

func Render

func Render(m Model, g Geometry) string

Render draws the whole screen. Pure over model and geometry, which is what allows exact golden tests with no TTY anywhere.

func RenderPicker

func RenderPicker(p Picker, geo Geometry) string

RenderPicker draws the list. Pure over the picker and the geometry, like every other surface here — the property that makes a screen testable.

func RenderStatusBar

func RenderStatusBar(m Model, g Geometry) string

RenderStatusBar draws the bottom bar: where you are, what has changed, and what is waiting.

One line, always, and never two. It is the only region of the screen that is true regardless of what the stream happens to be showing, so it gives ground by dropping segments rather than by wrapping — a second row would take a line from the layout the rest of the screen owns, and it would take it at exactly the moment the terminal was already too narrow.

Pure over the model and the geometry, like everything else that draws here. Segments are measured in display cells and assembled until they fit.

func ReplanPrompt

func ReplanPrompt(what string) string

ReplanPrompt asks for a fresh plan.

func Run

func Run(ctx context.Context, opts Options) error

Run starts the TUI. It takes the alternate screen, which is what a fixed panel requires — there is no way to hold a region while appending to terminal flow. The cost is scroll-back and mouse selection, reimplemented here.

func ScrollHint

func ScrollHint(m Model, g Geometry, top, total, height int) string

ScrollHint is the one-line note that the newest output is off screen.

Without it, a paused view is indistinguishable from a stalled agent: the screen stops changing either way.

func SessionList

func SessionList(list []protocol.Session, current string) string

SessionList renders `/resume`. Pure, so the shape is testable.

func ShadowedBuiltins

func ShadowedBuiltins(user config.CommandSet) []string

ShadowedBuiltins lists user commands that collide with a built-in name, so the override that did not happen can be reported rather than puzzled over.

func Spinner

func Spinner(frame int, unicode bool) string

Spinner returns the frame for a tick. Pure over the counter, so a golden test pins an exact frame instead of racing a clock.

func StreamLines

func StreamLines(m Model, g Geometry) []string

StreamLines renders the whole stream, not just what fits.

Everything, because the window is taken from it afterwards: rendering only the tail is what made scrolling impossible, since there was nothing above the screen to scroll back to.

func Window

func Window(m Model, g Geometry, body []string) (visible []string, top, total, height int)

Window returns the visible slice of the stream and where it sits.

The scroll position is clamped here rather than trusted, because the content grows underneath it: a position that was valid one event ago can be past the end now, and a client that trusted it would render blank.

Types

type Builtin

type Builtin struct {
	Name string
}

Builtin is a command the client itself answers.

Built-in commands are client surface, not configuration: which ones exist is a product decision. Configuration owns only the discovery and expansion of user commands.

type CommandKind

type CommandKind int

Kind classifies a resolved input line.

const (
	// CmdText is ordinary input, or a user command already expanded into the
	// text the user would have typed.
	CmdText CommandKind = iota
	CmdBuiltin
	CmdUnknown
	// CmdShell is a line the person runs themselves, written after `!`.
	CmdShell
)

type Completion

type Completion struct {
	Name        string
	Args        string
	Description string
}

Completion is one candidate for the `/` menu.

func Complete

func Complete(input string, user config.CommandSet, lang Lang) []Completion

Complete lists the commands matching what has been typed.

Only for a line that is a bare `/` prefix: once there is an argument the user has chosen, and a menu that stays open is a menu in the way. Built-ins come first because a user command can never shadow one, so offering them mixed together would suggest a competition that does not exist.

type CopyState

type CopyState struct {
	Active bool
	Anchor int
	Head   int
}

CopyState is the selection while copy mode is open.

Two line indices into the rendered stream rather than a start and a length, because the selection is dragged in both directions and an anchor that moves is how a selection ends up off by one at one end.

func (CopyState) Contains

func (c CopyState) Contains(i int) bool

Contains reports whether a rendered line is inside the selection.

func (CopyState) Range

func (c CopyState) Range() (int, int)

Range returns the selected lines, low first.

type Depth added in v0.5.0

type Depth int

Depth is how much colour the terminal can take.

const (
	// DepthTrue is 24-bit. The zero value, because it is what the palette is
	// authored in and what a terminal that says nothing about itself but has
	// colour enabled almost always is today.
	DepthTrue Depth = iota
	// Depth256 is the xterm cube plus the grey ramp.
	Depth256
)

func ColorDepth added in v0.5.0

func ColorDepth(env func(string) string) Depth

ColorEnabled decides whether to emit escapes at all.

NO_COLOR is honoured because it is the convention users already know, and TERM=dumb because a terminal that says it cannot should be believed. An explicit DCODE_COLOR wins over both: it is the user answering for their own terminal, and this is exactly the case where they know better than the heuristics. ColorDepth decides how much colour the terminal can take.

Truecolor unless the terminal says otherwise. COLORTERM is the only signal anybody actually sets for it, and the fallback is the 256-colour cube rather than sixteen: this palette is violet-grey text on a violet ground, and sixteen colours cannot draw either. A terminal below 256 gets no colour at all, which is a screen this product is tested to be readable on.

type Entry

type Entry struct {
	Kind     Kind
	Tool     string
	Target   string
	Summary  string
	Detail   string
	IsError  bool
	Expanded bool
	Seq      uint64
	// Duration is how long the tool took, as the daemon measured it.
	Duration time.Duration
	// Running marks a tool call that has not reported back yet, which is what
	// the spinner attaches to.
	Running bool
	// Typed marks a call the person asked for through `!`, which is what makes
	// its output open on its own. See protocol.ToolRequested.Typed.
	Typed bool
	// CallID is the tool call this entry is, so a result and a progress report
	// land on the call they belong to.
	//
	// A completion used to be matched to the LAST running entry, which is right
	// exactly while one call runs at a time. With two in flight the first
	// result landed on the second call's line — the numbers were real and the
	// row they appeared on was not.
	CallID string
	// Arriving marks a call whose arguments are still coming from the model.
	// It is running, but not yet running: nothing has been executed, and the
	// count is bytes of a request rather than work done.
	Arriving bool
	// Done and Total are how far a running call has got, when it says.
	Done, Total int
	// Owns is what a delegated child declared it would write, taken from the
	// call's input. It is the boundary the child was given, and the screen has
	// no other way to show what a child was allowed to touch.
	Owns []string
	// At is when the daemon said this happened, taken from the event. The
	// session pane lists recent calls by the clock, and a client that stamped
	// them on arrival would date a replayed session to the moment it was
	// replayed.
	At time.Time
	// Approval is the crossing this entry asks about, and Decision what was
	// answered. Both live on the entry rather than only on Model.Pending: the
	// question is part of the transcript, and "what did I approve?" is a
	// question somebody asks an hour later, when Pending is long gone.
	Approval *protocol.ApprovalRequest
	Decision protocol.ApprovalDecision
	// Plan is the current plan, on the one KindPlan entry. It is a snapshot of
	// Model.Plan rather than a copy that can drift: both are written from the
	// same event, in the same reduction.
	Plan []protocol.PlanItem
	// Added and Removed are the line counts the tool reported, kept as numbers.
	//
	// Summary already renders them, but as a sentence. The sidebar needs the
	// figure, and reading it back out of the sentence is the thing the protocol
	// comment forbids in as many words: a client that parses output to rebuild
	// these numbers breaks silently the day the wording changes.
	Added, Removed int
	// Diff is the unified diff of a change. When present it is what the
	// expansion shows: it is what gets reviewed, and the tool's prose summary
	// says nothing a reviewer needs.
	Diff string
	// StartedAt and Closed belong to a thought: it streams live while open and
	// collapses to one line once the turn moves on, because thinking runs
	// several times the length of the answer and would otherwise bury it.
	StartedAt time.Time
	Closed    bool
}

Entry is one line of the stream, with its detail available on demand.

type FileRow added in v0.2.0

type FileRow struct {
	// Path is the whole path; Label is what the row shows, already indented
	// and already compacted where a folder had a single child.
	Path  string
	Label string
	Depth int
	State FileState
	// Added and Removed are the line counts the tool reported, never parsed
	// back out of its summary. Both, because the diff pane draws a bar of the
	// two against each other and one of them is not a proportion.
	Added, Removed int
	// Folder marks a row that stands for a directory rather than a file.
	Folder bool
}

FileRow is one path the turn touched, ready to draw.

func FileTree added in v0.2.0

func FileTree(entries []Entry) []FileRow

FileTree lays the touched paths out as a folder row followed by its files.

Two levels, not a full tree, and the column's width is the reason: it is twenty to thirty characters, and every level of indentation is two of them taken from the only part that identifies a file — its name. A folder row carries its whole path, which is the design's single-child compaction taken to its conclusion: `internal/tui/` on one line rather than two rows nobody needs to see on their own.

type FileState added in v0.2.0

type FileState int

FileState is what the last event to touch a path said about it.

const (
	// FileReading and FileWriting are calls still in flight.
	FileReading FileState = iota
	FileWriting
	// FileDone is a call that reported back, and FileFailed one that did not
	// or came back an error.
	FileDone
	FileFailed
)

type Geometry

type Geometry struct {
	Width  int
	Height int

	// The side column: the diff pane over the session pane, on the right.
	//
	// Two fifths of the terminal, between a floor and a ceiling — the design's
	// split is 57/43 and this is that, clamped. It is wider than the file list
	// it replaces because it holds two panes and a bar, and it earns the width
	// the file list did not: nothing in it repeats the stream.
	RailMinWidth      int
	RailMaxWidth      int
	RailMinTotalWidth int
	RailMode          RailMode

	// DiffPreviewLines is how much of a diff shows without asking. A diff is
	// what gets reviewed, so some of it is always visible — but a whole-file
	// rewrite must not bury the conversation it belongs to.
	DiffPreviewLines int
	DiffMaxLines     int
	// CompletionRows is how many candidates the `/` menu shows at once.
	CompletionRows int
	// ThoughtLines is how much of a live thought stays on screen. Enough to
	// read where it is going, not enough to push the work off the top.
	ThoughtLines int
	Unicode      bool
	// ActivityVerbs draws the gerund beside the running tool on the activity
	// line. Presentation, resolved at the edge like Unicode and the palette,
	// because this package never reads the environment.
	ActivityVerbs bool
	Palette       Palette
}

Geometry is the terminal size and the layout knobs.

func DefaultGeometry

func DefaultGeometry(w, h int) Geometry

DefaultGeometry returns the documented defaults.

func (Geometry) ShowRail added in v0.2.0

func (g Geometry) ShowRail(hasContent bool) bool

ShowRail reports whether the side column is drawn.

Shown by default on a terminal wide enough, and this REVERSES the default set earlier today. The reason it was hidden was measured and stands: the file list it replaced was a second copy of what the stream had just said, and twenty-six columns is a lot to pay for a repetition.

These two panes are not that. A bar of added against removed, a context gauge, what the person allowed of what was asked, the last calls by the clock — none of it is anywhere else on the screen. The objection was never "a column is expensive", it was "that column bought nothing".

Below RailMinTotalWidth it goes, because two fifths of a narrow terminal is two fifths taken from a stream that has none to spare, and an explicit choice still wins in both directions.

func (Geometry) StreamWidth

func (g Geometry) StreamWidth(showRail bool) int

StreamWidth is what the stream gets once the columns have taken theirs.

One function, read by the layout and by the renderer both. Two places computing a width is the defect; where it shows up is only the symptom, and this family has paid for that once already with a painted frame.

type Kind

type Kind string

Kind classifies a stream entry.

const (
	KindUser      Kind = "user"
	KindAssistant Kind = "assistant"
	KindTool      Kind = "tool"
	KindError     Kind = "error"
	KindNote      Kind = "note"
	// KindReasoning is the model thinking, which is not the model answering.
	KindReasoning Kind = "reasoning"
	// KindApproval is a boundary crossing put to the person: what is being
	// asked, and — once answered — what they said.
	KindApproval Kind = "approval"
	// KindPlan is the plan the model is working through, drawn where it first
	// appeared and always showing the current one.
	KindPlan Kind = "plan"
	// KindCompletion is what was and was not checked when the turn ended.
	//
	// Its own kind rather than a note, because it is the one line on screen the
	// model's prose cannot contradict: the text may claim success, the seal is
	// derived from what actually ran.
	KindCompletion Kind = "completion"
)

type Lane added in v0.5.0

type Lane int

Lane is which of the three things a row is: what you asked, what the model did on the way, and what it says.

It is the one idea worth taking whole from the v2 design, and the reason is what a long turn looks like: prose and tool calls alternate down the screen with nothing structural telling them apart, so catching up means reading every row to find out which rows were worth reading. With a lane in the gutter the eye can run down the answer lane alone.

It costs NOTHING. Every row of the stream already reserved two columns — the selection marker, or two spaces where there was none. The lane takes the first of them and the marker keeps the second.

const (
	// LaneProcess is the zero value because it is what an unknown kind should
	// read as: work on the way to an answer, not an answer.
	LaneProcess Lane = iota
	LaneYou
	LaneAnswer
	// LaneAsk is a question put to the person, and the design gives it a lane
	// of its own for the reason it deserves one: it is the only row on the
	// screen that will not move until somebody does something.
	LaneAsk
)

type Lang

type Lang string

Lang is a declared interface language.

const (
	PtBR Lang = "pt-BR"
	En   Lang = "en"
)

func Languages

func Languages() []Lang

Languages are the declared languages, for the coverage guard.

func Resolve

func Resolve(get func(string) string) Lang

Resolve picks the interface language.

DCODE_LANG beats LC_ALL, which beats LANG. An unknown language resolves to the fallback WITHOUT an error: refusing to start over an unrecognised locale would be a worse answer than showing Portuguese.

type LoopArgs added in v0.10.0

type LoopArgs struct {
	// Task is what to do, in the person's own words, when they said.
	//
	// Everything after the path that is not a flag. `/loop specs/x` is the
	// command doing its job on its own; `/loop specs/x refaça só o header` is
	// the same job narrowed, and refusing the second was the command telling
	// someone their sentence was a mistyped flag.
	Task string
	// Spec is the folder holding tasks.md, as the user typed it. Resolving it
	// is the daemon's job: it owns the filesystem, and a client that resolved
	// the path would be asserting something about a disk it may not share.
	Spec string
	// Protect are globs added to whatever the spec declares.
	Protect []string
	// Qualify marks the session that works out what done means for Spec,
	// rather than the one that does the work.
	//
	// The LOOP sets it, never the model: reading, projecting and qualifying
	// are what the loop does before it executes, and a model that chose when
	// to qualify would be choosing when to be measured.
	Qualify bool
	// Goal marks an argument that is a sentence rather than a path.
	//
	// `/loop implemente todas as specs pendentes` is what someone types when
	// they mean the whole backlog, and the first version made `implemente`
	// into a folder name and then failed to read `implemente/tasks.md`. Prose
	// became a path — the same defect as prose becoming a criterion, in the
	// other direction.
	Goal bool
}

LoopArgs is what `/loop` was given, after parsing.

func ParseLoopArgs added in v0.10.0

func ParseLoopArgs(args string) (LoopArgs, error)

ParseLoopArgs splits the argument of `/loop`. Pure, and strict.

Strict because a mistyped flag that is silently ignored produces a session measured against a definition of done the person did not ask for, and they find out at the end of the turn. An unknown flag stops the command.

type Model

type Model struct {
	SessionID string
	Workspace string
	Model     string
	Sandbox   string
	// Mode is the session behavioural mode (plan, assist, auto). Empty when
	// the session has not yet announced its mode — older sessions reply
	// without it and a missing label is the honest rendering.
	Mode  string
	State protocol.SessionState

	Entries []Entry
	Plan    []protocol.PlanItem
	// Rounds and InFlight are where the turn is against its ceilings, as the
	// daemon reports them. Kept as the pair rather than as a percentage: the
	// question a person asks of a ceiling is how many are left, and a share
	// cannot answer it.
	Rounds, MaxRounds     int
	InFlight, MaxInFlight int
	// Nav is the session list's cursor while the rail has the keyboard.
	Nav RailNav
	// Navigating says the transcript has the keyboard.
	//
	// A MODE, and that is the whole point of it. The design's footer offers
	// `j/k move` and `t theme`, which are letters, and a letter on a line where
	// you type is the defect this product has fixed twice. Inside a mode that
	// owns the keyboard a letter is safe — the approval modal and the session
	// list already work that way — and the design implies exactly this by
	// putting a NAV badge in the footer at all.
	Navigating bool
	// Sessions is what this workspace has recorded, for the sidebar. Passed in
	// by the caller like the language and the command set: the client reads no
	// disk, and a list it went and fetched itself would be a second answer to a
	// question the edge already answers for `dcode -r`.
	Sessions []SessionChoice
	// Lang is the interface language, resolved once when the client starts.
	Lang Lang
	// Copy is the selection while copy mode is open. The alternate screen costs
	// the terminal's own selection, and RN-1 says it has to be given back.
	Copy CopyState
	// Flash is a one-line notice shown until the next keystroke, for things
	// that happen and leave no other trace — a copy landing, for instance.
	Flash string
	// Leaving is armed by the first ^C on an empty line and disarmed by any
	// other key. It is true exactly while the warning is on screen: a state a
	// person cannot see is a state they cannot reason about.
	Leaving bool
	// Verification is the seal of the last completed turn. Empty when the turn
	// had no definition of done.
	//
	// It is the guarantee that outlives a model claiming success in prose: the
	// text can lie, this is derived from what actually ran.
	Verification string
	Pending      *protocol.ApprovalRequest
	// The diff accumulated in this worktree, summed from what each tool
	// reported rather than parsed back out of its text.
	DiffAdded   int
	DiffRemoved int
	DiffFiles   int

	// Asked and Allowed count boundary crossings put to the person and the
	// ones they let through.
	Asked, Allowed int
	// ContextTokens is what the context costs now, as the daemon measured it.
	// InputTokens beside it is CUMULATIVE and is the turn's cost, not its size.
	ContextTokens int

	InputTokens  int
	OutputTokens int
	CacheTokens  int
	ContextPct   int

	LastSeq uint64

	// Window is the model's context window, so a token count can become a
	// percentage. Zero means the daemon did not report one.
	Window int

	// TurnStartedAt is when the running turn began, measured by the client.
	// Elapsed time is client-local on purpose: it must tick between events,
	// and a server-sent timestamp would only be right at the instant it
	// arrived.
	TurnStartedAt time.Time
	// Frame advances on every animation tick. Render stays pure by reading it
	// rather than a clock.
	Frame int
	// Now is the clock the view was rendered against, for elapsed time.
	Now time.Time

	// Client-local state. Nothing here is session state (RN-11).
	Cursor    int
	ScrollTop int
	// Follow keeps the newest output on screen. It turns off the moment the
	// user scrolls up — reading something while the stream pushes it away is
	// the single most irritating thing a live log can do — and back on when
	// they return to the bottom.
	Follow bool
	Queue  []string
	// Attached are pictures waiting for the next question. A picture with no
	// question is a turn the model has to guess the point of.
	Attached []protocol.TurnImage
	Input    string
	// InputCursor is the caret position within Input, in runes.
	InputCursor int
	// History is what the user has sent, newest last, with HistoryAt as the
	// position being browsed. Client-local: it is what this person typed at
	// this terminal, not session state.
	History   []string
	HistoryAt int
	// Draft holds what was typed before the user started browsing history, so
	// coming back out of the history does not lose it.
	Draft string

	// Completions is the open `/` menu, with CompletionAt as the highlighted
	// row. Empty means no menu — the menu is a consequence of what is typed,
	// never a mode the user has to leave.
	Completions  []Completion
	CompletionAt int
	// CompletionsOff suppresses the menu until the line changes, which is how
	// Esc closes it without also clearing the line. Any edit revives it —
	// dismissing the menu answered for the line as it was, not for every line
	// that follows.
	CompletionsOff bool
	// contains filtered or unexported fields
}

Model is the view state, derived entirely from the event log.

func NewModel

func NewModel(sessionID, workspace, model, sandbox string, lang Lang) Model

NewModel builds an empty view. NewModel builds the client state.

The language is a parameter rather than resolved here, for the same reason the palette is not built here: this package renders, and the environment is read once, at the edge, by whoever starts the client. A zero Lang lands on the fallback, which is the documented behaviour and not an accident.

func (Model) AcceptCompletion

func (m Model) AcceptCompletion() Model

AcceptCompletion puts the highlighted command on the line.

A trailing space when the command takes arguments: the user's next keystroke is the argument, and making them type the separator is a small tax on every single use.

func (Model) Apply

func (m Model) Apply(ev protocol.Event) Model

Apply folds one event into the view. Pure: the same sequence of events always produces the same model, which is what makes replay equal live observation.

func (Model) Backspace

func (m Model) Backspace() Model

Backspace deletes before the caret.

func (Model) CloseCompletions

func (m Model) CloseCompletions() Model

CloseCompletions hides the menu until the line changes.

func (Model) DeleteForward

func (m Model) DeleteForward() Model

DeleteForward deletes under the caret.

func (Model) DeleteWord

func (m Model) DeleteWord() Model

DeleteWord removes the word before the caret, trailing spaces included.

func (Model) DrainQueue

func (m Model) DrainQueue() (Model, string)

DrainQueue returns the queued messages joined into one turn.

One turn, not several: multiple turns would violate the one-turn-per-session rule and reorder the event log.

func (Model) Enqueue

func (m Model) Enqueue(text string, max int) (Model, bool)

Enqueue accepts input while a turn is running.

The queue is client-local: the protocol refuses a concurrent turn, so waiting here is what turns a refusal into a usable experience instead of an error the user has to work around.

func (Model) EnsureCursorVisible

func (m Model) EnsureCursorVisible(g Geometry) Model

EnsureCursorVisible scrolls the window so the selected entry is on screen.

Moving a cursor you cannot see is how a keypress feels broken: something happened, and nothing on screen changed.

func (Model) EnterCopy

func (m Model) EnterCopy(lastLine int) Model

EnterCopy opens copy mode anchored on the cursor, or on the last line.

func (Model) ExtendCopy

func (m Model) ExtendCopy(delta, lastLine int) Model

ExtendCopy moves the head of the selection, keeping the anchor.

The anchor stays put so dragging back past the start shrinks the selection rather than inverting it — an anchor that moves is how a selection ends up off by one at one end.

func (Model) HistoryNext

func (m Model) HistoryNext() Model

HistoryNext walks forward, and past the newest entry returns the draft.

func (Model) HistoryPrev

func (m Model) HistoryPrev() Model

HistoryPrev walks back through what was sent.

func (Model) Insert

func (m Model) Insert(s string) Model

Insert types text at the caret.

func (Model) LeaveCopy

func (m Model) LeaveCopy() Model

LeaveCopy closes it.

func (Model) MoveCompletion

func (m Model) MoveCompletion(delta int) Model

MoveCompletion walks the menu, wrapping at both ends.

func (Model) PlanCounts

func (m Model) PlanCounts() (done, total, blocked int)

PlanCounts returns done, total and blocked.

func (Model) PlanSummary

func (m Model) PlanSummary() string

PlanSummary is the footer line.

The same string is used in the panel and in the status bar when the panel collapses. One formulation, or the two drift apart at the first change.

func (Model) Refresh

func (m Model) Refresh(user config.CommandSet) Model

Refresh recomputes the menu from the current line.

Derived rather than toggled: a menu with its own open/closed state drifts out of step with the text the moment anything else edits the line.

func (Model) Remember

func (m Model) Remember(text string) Model

Remember records a sent line. Consecutive duplicates are collapsed: pressing up twice should reach two different commands, not the same one again.

func (Model) RemoveFromQueue

func (m Model) RemoveFromQueue(i int) Model

RemoveFromQueue drops a queued message before it is sent.

func (Model) ScrollBy

func (m Model) ScrollBy(lines int, g Geometry) Model

ScrollBy moves the window and decides whether to keep following.

Scrolling up stops the follow: reading something while the stream pushes it off the screen is the single most irritating thing a live log can do. Arriving back at the bottom resumes it, because that is what the user just asked for by going there.

func (Model) ScrollToBottom

func (m Model) ScrollToBottom(g Geometry) Model

ScrollToBottom jumps to the newest output and resumes following.

func (Model) ScrollToTop

func (m Model) ScrollToTop() Model

ScrollToTop jumps to the beginning of the session.

func (Model) SetInput

func (m Model) SetInput(s string) Model

SetInput replaces the line and puts the caret at its end.

func (Model) ShowEmptyState

func (m Model) ShowEmptyState() bool

ShowEmptyState reports whether the splash should render.

It disappears on the first turn and never returns: a persistent splash steals height from the stream, which is the scarce resource on screen. A resumed session never shows it, because someone resuming wants to see where they left off.

func (Model) ToggleAt

func (m Model) ToggleAt(i int) Model

ToggleAt expands or collapses one entry.

type Options

type Options struct {
	SessionID string
	Workspace string
	Model     string
	Sandbox   string
	// Window is the model's context window, so a token count can become the
	// percentage a person can act on.
	Window int
	// Backlog is the sequence the session's log already reaches when the client
	// attaches — everything at or below it is history being replayed, not
	// something happening now.
	//
	// Continuing a conversation writes the whole of the old log into the new
	// session, so attaching to one replays every event of it: 3544, on a real
	// session of this machine. Each arrives as its own message and Bubble Tea
	// paints after every message, so resuming redrew the screen 3544 times with
	// the window following its own end — which is the screen that would not
	// stop scrolling.
	Backlog uint64
	// From is the earliest sequence the session still holds. Zero means 1:
	// a client that assumes the beginning is always available asks for events
	// that retention has already dropped, and gets a refusal instead of the
	// conversation.
	From      uint64
	Transport Transport
	Geometry  Geometry
	QueueMax  int
	// Lang is the interface language, resolved once by the caller. Zero lands
	// on the fallback.
	Lang Lang

	// Sessions is what this workspace has recorded, read once at start by the
	// caller. Only the conversations something was asked in — the rest is what
	// a record directory mostly holds, and burying four real ones under thirty
	// empty ones is what the picker already refuses to do.
	Sessions []SessionChoice

	// Commands is the user's discovered command set. Frozen at start, like the
	// instruction chain, so behaviour cannot change mid-session.
	Commands config.CommandSet
	// AcceptsImages says whether this session's model reads pictures. Passed
	// in rather than guessed: the client cannot know, and guessing wrong turns
	// a refusal it could have given into a provider error it cannot explain.
	AcceptsImages bool

	// Lookup answers `/config <key>`. Injected rather than read here, because
	// the client is not where configuration is resolved.
	Lookup func(key string) (string, bool)

	// Notice is the passive version check. It runs off the critical path and
	// its failure is silent by contract.
	Notice func(context.Context) string

	// Update applies a release, when the edge has one to apply. Resolved out
	// there rather than here for the reason the language and the palette are:
	// this package renders and never reads the environment, and an update
	// reads a good deal of it.
	Update func(context.Context) (UpdateResult, error)

	// Now is the clock for elapsed time. Injected so a test can assert an
	// exact duration instead of sleeping for one.
	Now func() time.Time
}

Options configure the program.

type Palette

type Palette struct {
	Enabled bool
	// Theme is the colours. The zero value is the neon theme, so a Palette
	// asked to draw without being told which theme draws the product's own
	// rather than nothing.
	Theme Theme
	// Depth is what the terminal can render. The zero value is truecolor,
	// which is what a terminal that answered COLORTERM has.
	Depth Depth
}

Palette turns a role into an escape sequence.

The zero value writes nothing at all, which is what makes colour removable: every rendering path is the same code, and a monochrome terminal simply gets empty strings rather than a second implementation.

func (Palette) Apply

func (p Palette) Apply(s Style, text string) string

Apply wraps text in a role.

Width is unaffected by design: every caller measures display cells before styling, and a style that changed the measured width would break the layout only on colour terminals — the hardest kind of bug to see in a screenshot.

func (Palette) Ground added in v0.5.0

func (p Palette) Ground() string

Ground is the escape that paints the screen behind everything, empty when colour is off or the theme has no ground of its own.

type Picker

type Picker struct {
	Choices []SessionChoice
	Cursor  int
	Lang    Lang
}

Picker is the list, and where the cursor is in it.

func NewPicker

func NewPicker(choices []SessionChoice, lang Lang) Picker

NewPicker opens on the newest, which is what somebody continuing almost always wants — the same reasoning that orders the list.

func (Picker) Chosen

func (p Picker) Chosen() string

Chosen is the session under the cursor, empty when there is nothing to choose. Empty has to be distinguishable from the first row, or cancelling silently opens a conversation.

func (Picker) Move

func (p Picker) Move(d int) Picker

Move walks the cursor and stops at both ends.

Wrapping would turn "one too far" into "somewhere else entirely", and this is a list where landing somewhere else opens the wrong afternoon's work.

type RailMode added in v0.2.0

type RailMode int

RailMode is the sidebar's visibility.

const (
	// RailAuto lets the width decide, and is the zero value again: the column
	// that replaced the file list carries things that are nowhere else, which
	// is what the file list did not.
	RailAuto RailMode = iota
	// RailHidden and RailShown are the user having thought about it.
	//
	// The rule that used to live beside this — the panel's own mode, mirroring
	// this one — is gone with the panel. The mirror was the defect: the two
	// columns answered "should I be here?" the same way with the same
	// threshold, and their two hundreds compounded, so crossing from 99 to 100
	// columns cost the conversation 46 of them at once.
	RailHidden
	RailShown
)

type RailNav added in v0.2.0

type RailNav struct {
	// Active says the rail has the keyboard.
	Active bool
	// Cursor is an index into the FILTERED list, never into the whole one.
	// Keeping it against the filtered view is what stops a narrowing filter
	// from leaving the cursor pointing past the end of what is on screen.
	Cursor int
	Filter string
	// Naming says the row under the cursor is being given a name, and Draft is
	// what has been typed. A separate mode rather than a second meaning for
	// the filter: one takes you to a conversation and the other changes it, and
	// a key that sometimes does each is a key nobody trusts.
	Naming bool
	Draft  string
}

RailNav is where the cursor is in the session list, and what has been typed to narrow it.

func (RailNav) Backspace added in v0.2.0

func (n RailNav) Backspace() RailNav

Backspace widens it, and drops the last rune rather than the last byte.

func (RailNav) BackspaceName added in v0.2.0

func (n RailNav) BackspaceName() RailNav

BackspaceName drops a rune from the draft.

func (RailNav) Chosen added in v0.2.0

func (n RailNav) Chosen(all []SessionChoice) string

Chosen is the conversation under the cursor, empty when the filter left nothing. Empty has to be distinguishable from the first row, or a filter that matched nothing would open the newest conversation instead of doing nothing.

func (RailNav) Escape added in v0.2.0

func (n RailNav) Escape() RailNav

Escape backs out of one thing at a time: the filter first, then the mode.

The same layering `esc` already has everywhere else here — close the expansion, then the selection, then the modal. Escape means "back out of what I opened", and the outermost thing opened is the last thing it abandons.

func (RailNav) Matches added in v0.2.0

func (n RailNav) Matches(c SessionChoice) bool

Matches reports whether a conversation survives the filter.

Case-insensitive on the title, because somebody typing a filter is remembering a phrase, not reproducing one.

func (RailNav) Move added in v0.2.0

func (n RailNav) Move(d, n_ int) RailNav

Move walks the cursor and stops at both ends.

It does not wrap, and the reason is the one the picker already writes down: wrapping turns "one too far" into "somewhere else entirely", and this is a list where landing somewhere else opens the wrong afternoon's work.

func (RailNav) StartNaming added in v0.2.0

func (n RailNav) StartNaming(all []SessionChoice) RailNav

StartNaming opens the name of the row under the cursor for editing, seeded with the name it already has.

Seeded with the NAME and not the derived title: offering the title as a draft would turn "give this a name" into "confirm the one you were given", and the first Enter would quietly promote a derived title into a chosen one.

func (RailNav) Type added in v0.2.0

func (n RailNav) Type(r string, all []SessionChoice) RailNav

Type narrows the filter and pulls the cursor back into range.

Back to the top rather than to the nearest surviving row: after typing, what the person is looking at is a different list, and holding a position in it would be holding a position in something they have not read.

func (RailNav) TypeName added in v0.2.0

func (n RailNav) TypeName(r string) RailNav

TypeName edits the draft.

func (RailNav) Visible added in v0.2.0

func (n RailNav) Visible(all []SessionChoice) []SessionChoice

Visible is the list as the filter leaves it.

type Resolved

type Resolved struct {
	Kind CommandKind
	Name string
	Args string
	Text string
}

Resolved is one input line, classified.

func ResolveInput

func ResolveInput(input string, user config.CommandSet) Resolved

ResolveInput classifies a line of input. Pure, and the only place that decides what a leading slash means.

A built-in always wins over a user command of the same name: a user file cannot shadow `/config` into meaning something else, because the moment it could, no advice about dcode would be true of any particular installation.

type SessionChoice

type SessionChoice struct {
	ID string
	// Title is derived from the first question; Name is what a person called
	// it. Kept apart so the listing can say which it is showing — a derived
	// title and a chosen one are not the same claim.
	Title string
	Name  string
	Turns int
	When  time.Time
}

SessionChoice is one recorded conversation, as somebody choosing needs to see it.

Deliberately not the server's summary type: the client renders and does not learn what a session is. Whoever opens the picker maps one to the other, and that mapping is the only place the two vocabularies meet.

type Strings

type Strings struct {
	// Working is the activity line with no tool running — the one plain word
	// it falls back to. Deliberately not one of the rotating verbs: see
	// activity.go.
	Working string
	// RailFiles heads the sidebar, and RailTouchedOne/Many count what the turn
	// has touched — the header still says something when the column is narrow.
	ChildOne      string
	ChildMany     string
	ChildOwns     string
	ChildNoAnswer string
	ChildUnnamed  string
	PanelRounds   string
	PanelInFlight string
	RailHidden    string
	RailFiles     string
	RailSessions  string
	RailFilter    string
	RailNaming    string
	RailNoMatch   string

	// The side column.
	SideDiff       string
	SideSession    string
	SideNothingYet string
	SideContext    string
	SideAllowed    string
	SideRecent     string
	SideBarScale   string

	// The lane legend.
	LaneYou     string
	LaneProcess string
	LaneAnswer  string

	// The nav bar.
	NavBadge     string
	NavSessions  string
	NavColumn    string
	NavKeys      string
	NavEnter     string
	NavMove      string
	NavOpen      string
	NavPrompt    string
	NavLeave     string
	SideToolOne  string
	SideToolMany string
	// While the history of a continued conversation is being read.
	Loading    string
	LoadedOne  string
	LoadedMany string

	// The context filling up, and the summary when it does.
	ContextFilling string
	Compacted      string
	CompactedCount string

	// The approval modal. It was written in English literals — the ONE screen
	// that asks whether a boundary may be crossed, in a language the reader may
	// not have. Consent given to a sentence somebody could not read is not
	// consent.
	ApprovalCrosses        string
	ApprovalNetwork        string
	ApprovalEnter          string
	ShellHint              string
	LeavingTakesTwo        string
	UpdateApplied          string
	UpdateCurrent          string
	UpdateUnavailable      string
	ApprovalCrossing       string
	ApprovalRule           string
	ApprovalAnswered       string
	ApprovalDenied         string
	ApprovalAllowedOnce    string
	ApprovalAllowedSession string
	ApprovalAllowedProject string
	ApprovalAllowedAlways  string
	KeyDeny                string
	KeyAllow               string
	KeySession             string
	KeyNo                  string
	KeyOnce                string
	KeyProject             string
	KeyAlways              string
	// PanelPlan is the panel's own heading, which was a literal too.
	PanelPlan string
	// PlanOf and PlanBlockedCount build the plan's footer count.
	PlanOf           string
	PlanBlockedCount string
	SessionsMoreOne  string
	SessionsMoreMany string
	SessionsKeys     string
	RailTouchedOne   string
	RailTouchedMany  string

	// LineOne and LineMany count hidden lines in a collapsed body, and
	// ExpandHint says how to see them. All three because the hint said
	// "Tab expande" in Portuguese next to a count in English, on one line, in
	// both interfaces.
	LineOne    string
	LineMany   string
	ExpandHint string

	// WorkingInterrupt is the way out, on the activity line. Its own string
	// rather than Interrupt, which is the `esc` hint somewhere else: two keys
	// with one sentence between them is how a hint ends up naming the wrong
	// key in one of the languages.
	WorkingInterrupt string

	// Status line
	VerifiedLabel    string
	NotVerifiedLabel string
	UnverifiedLabel  string

	// Completion report
	VerifiedSummary     string // takes a count
	NotVerifiedSummary  string // takes the failing names
	NothingCouldCheck   string
	ChangedAfterCheck   string
	CompletionMet       string
	CompletionUnmet     string
	CompletionUnchecked string
	CompletionMeasure   string

	// Approval
	ApprovalDeny         string
	ApprovalAllowOnce    string
	ApprovalAllowSession string
	ApprovalEnterDenies  string
	ApprovalHeading      string

	// Help. The key and command descriptions live here too: a /help with
	// translated headings and English descriptions is worse than an untranslated
	// one, because it reads as a bug rather than as a language.
	HelpCommands       string
	HelpApprovals      string
	HelpKeys           string
	HelpYours          string
	KeyEnter           string
	KeyNewline         string
	KeyPasteImage      string
	CmdUndo            string
	CmdUpdate          string
	CmdImage           string
	CmdImageArgs       string
	ImageUsage         string
	ImageAttached      string
	ImageFailed        string
	ImageUnsupported   string
	ImagePasted        string
	ImageTooBig        string
	ClipboardEmpty     string
	ClipboardMissing   string
	UndoRestored       string
	UndoRefused        string
	UndoNothing        string
	UndoFailed         string
	KeyArrows          string
	KeyPage            string
	KeyTab             string
	KeyEsc             string
	KeyPanel           string
	KeyDequeue         string
	KeyEditing         string
	KeyInterrupt       string
	KeyQuit            string
	CmdHelp            string
	CmdInit            string
	CmdClear           string
	CmdPlan            string
	CmdPlanArgs        string
	CmdConfig          string
	CmdConfigArgs      string
	CmdModel           string
	CmdModelArgs       string
	CmdResume          string
	CmdResumeArgs      string
	CmdMode            string
	CmdModeArgs        string
	CmdModeCurrent     string // takes a mode name
	CmdModeUnnamed     string // the boundary in force is none of the three
	CmdModeUnknown     string // takes the name that is not a mode
	CmdLoop            string
	CmdLoopArgs        string
	CmdLoopUsage       string
	CmdLoopFlag        string // takes the flag that is not one
	CmdLoopOpened      string // takes the spec path and the criterion count
	CmdLoopEmpty       string // takes the spec path
	CmdLoopProposed    string // takes the criterion count, the file and the spec path
	CmdLoopQualifying  string // takes the spec path
	LoopNoSpecs        string
	LoopPlanHead       string // takes pending and total
	LoopSpecPending    string // takes unmet and total criteria
	LoopSpecDone       string // takes the criterion count
	LoopSpecNoCriteria string
	LoopSpecUnreadable string // takes the error

	// CLI. The usage block is one string per language rather than a field per
	// line: it is prose with alignment, and cutting it into thirty fields
	// would make it harder to translate well, not easier.
	Usage string

	// Copy mode
	CopySelected string // takes a line count
	CopyKeys     string
	CopyDone     string
	CopyEmpty    string

	// Empty state and general
	Interrupt string
	Queued    string
	// The bottom bar names what it counts, so the numbers survive a terminal
	// with no colour to group them.
	BarFiles   string
	BarWaiting string
	NoPlan     string
	// The session picker.
	PickerTitle     string
	PickerKeys      string
	PickerEmpty     string
	PickerUntitled  string
	PickerYesterday string
	// PickerTurnOne and PickerTurnMany are the noun, and plural() puts the
	// number in front. It used to be one string reading "%d turn(s)", which is
	// the parenthetical plural nobody ever comes back to replace — and it now
	// appears on every row of the conversation list rather than only in the
	// picker, where it was easy not to look at.
	PickerTurnOne  string
	PickerTurnMany string

	// Resumed opens a continued conversation. It takes the session it came
	// from and how many turns, in that order.
	Resumed string
}

Strings is every piece of text the client composes itself.

What is NOT here is anything the model reads: tool descriptions, tool error text, and the doctrine. RN-3 of behavior-definition makes tool error text a behaviour surface — the layer where recovery is taught — so translating it is changing the prompt. And the behavioural thresholds were measured in English, so translating invalidates the measurement without breaking anything visibly. The client translates only what it wraps around them.

func Text

func Text(l Lang) Strings

Text returns the catalogue for a language.

The language lives on the Model and nowhere else. Carrying it on Geometry as well would be two sources for one fact, and the first thing that produces is a status line in one language and a report in another.

An undeclared language returns the fallback rather than an empty struct: a blank interface is the one outcome worse than the wrong language.

type Style

type Style uint8

Style is one visual role, not one colour.

Roles rather than colours because the palette has to answer "what does this mean" in three different terminals. A caller that asks for "red" has already decided something the theme should decide.

const (
	StyleNone Style = iota
	StyleDim
	StyleBold

	// The text hierarchy, as roles rather than as one StyleDim meaning five
	// things at forty-odd call sites.
	//
	// A terminal has exactly three weights that survive an unknown background:
	// bold, normal, and SGR 2. Anything else is a hard-coded grey, and a grey
	// picked for a dark theme is unreadable on a light one — which is why the
	// design's five-step scale does not survive contact with a real terminal,
	// and why this is six roles sharing three weights and one colour.
	//
	// What the roles buy is that the mapping is ONE decision in one table. The
	// first thing that decision changed: prose is no longer dim.
	StyleProse   // the model's sentences: the thing on screen to be READ
	StyleCode    // a technical term inside a sentence
	StyleHeading // a section label
	StyleMeta    // a fact qualifying another: a count, a duration, a summary
	StyleHint    // a key somebody may press
	StyleChrome  // a rule, a gutter, a frame: present, never read

	// The lanes. Colours of their own rather than reuse: a lane is not a
	// state, and borrowing StyleOK for the answer lane would make every
	// answer read as a success.
	StyleLaneYou
	StyleLaneProcess
	StyleLaneAnswer
	// StyleTrack is the unfilled part of a bar.
	StyleTrack
	StyleAccent  // the product's own mark
	StyleAdded   // a diff line that arrived
	StyleRemoved // a diff line that left
	StyleError
	StyleWarn
	StyleOK
	StyleDanger // full-access, and nothing else
	StyleCursor
	// The three amber tones and the terracotta of the mark. They exist so the
	// mascot renders as itself in the terminal; nothing else uses them, because
	// the moment a second thing does, the eye stops being a marker.
	StyleHighlight
	StyleBody
	StyleShadow
	StyleEye
	// StyleOnAccent is amber ground with near-black text. The design gives it
	// to structure and to nothing else: two of them side by side would say that
	// everything is structure.
	StyleOnAccent
)

func ContextStyle

func ContextStyle(pct int) Style

ContextStyle grades the context meter.

Colour here is a warning, not decoration: the number matters only when it is close to the point where history starts being summarised away.

func DiffStyle

func DiffStyle(line string) Style

DiffStyle classifies one line of a unified diff.

func VerificationLabel

func VerificationLabel(v string, lang Lang) (string, Style)

VerificationLabel is the seal shown on the status line.

Short enough to survive beside everything else, and worded so the failing states read as failing without colour: a monochrome terminal, a screenshot in a bug report, and a colour-blind reader all have to get the same answer.

type Theme added in v0.5.0

type Theme struct {
	Name string
	// Ground is painted behind every row. Empty means the terminal's own.
	Ground rgb
	// Role is the colour of each role. A role absent from the map is drawn
	// without colour, which is how a theme says "normal weight" for prose.
	Role map[Style]paint
}

A theme is the whole palette as colours, and neon is the one this interface is drawn in.

Until now the roles mapped to a handful of ANSI codes chosen to sit politely inside whatever the terminal's own theme was. That politeness is what made the screen read as grey: three weights and one amber, over a background the product did not choose, is not a palette — it is an absence of one.

A theme carries its own ground. That is the decision, and it is not free: the interface stops inheriting the terminal's colours and starts owning them. It is what the design asks for, and what makes an accent an accent — amber over an unknown background is a colour; amber over #120d24 is a signal.

Colour that is switched off gets NONE of this: Palette{} still writes nothing at all, background included, and the screen falls back to the terminal's own. That path is not a second implementation, it is the same code with an empty table.

func Neon added in v0.5.0

func Neon() Theme

Neon is the interface's own palette: violet ground, magenta mark, teal for what worked, amber for the person, coral for what did not.

The values are the design's, not near neighbours of them. A palette copied approximately is a palette that reads as a copy.

func NextTheme added in v0.5.0

func NextTheme(t Theme) Theme

NextTheme is the one after this, wrapping. Wrapping is right here and wrong in the conversation list, and the difference is the cost of overshooting: one more press, against opening somebody else's afternoon.

func Themes added in v0.5.0

func Themes() []Theme

Themes are the four the design carries, in the order `t` cycles them. Neon first: it is the one this interface is drawn in.

type Transport

type Transport interface {
	CreateSession(ctx context.Context, req protocol.CreateSessionRequest) (protocol.Session, error)
	ListSessions(ctx context.Context) ([]protocol.Session, error)
	ListSpecs(ctx context.Context, workspace string, measure bool) ([]protocol.SpecFolder, error)
	CommitDone(ctx context.Context, id string) (protocol.CommitDoneResponse, error)
	GetSession(ctx context.Context, id string) (protocol.Session, error)
	Submit(ctx context.Context, id, text string, images ...protocol.TurnImage) error
	Interrupt(ctx context.Context, id string) error
	Steer(ctx context.Context, id, text string) error
	Exec(ctx context.Context, id, command string) error
	Undo(ctx context.Context, id string) (protocol.UndoResult, error)
	RenameSession(ctx context.Context, id, name string) error
	Resolve(ctx context.Context, id, approvalID string, d protocol.ApprovalDecision) error
	SetMode(ctx context.Context, id, mode string) error
	Subscribe(ctx context.Context, id string, from uint64) (<-chan protocol.Event, <-chan error)
}

Transport is what the TUI needs from a daemon. An interface rather than the concrete client so the program can be driven in tests without a socket.

type UpdateResult added in v0.6.0

type UpdateResult struct {
	From, To string
	// Applied is false when the running version was already the latest. Not an
	// error — nothing was wrong — and not silence either.
	Applied bool
}

UpdateResult is what an update attempt did.

The client composes the sentence from it rather than being handed one: a screen written at the edge is a screen that stays in one language, and this interface is bilingual.

Jump to

Keyboard shortcuts

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