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
- Variables
- func ActivityVerb(tool string, frame int, l Lang) string
- func ActivityVerbsEnabled(env func(string) string) bool
- func AtBottom(top, total, height int) bool
- func BodyHeight(m Model, g Geometry) int
- func ClipboardImage() ([]byte, string, error)
- func ColorEnabled(env func(string) string) bool
- func ContextLabel(used, window int) string
- func CopyHint(c CopyState, lang Lang) string
- func CopyText(lines []string, c CopyState) string
- func FormatDuration(d time.Duration) string
- func HelpText(user config.CommandSet, lang Lang) string
- func InputHeight(m Model, g Geometry) int
- func InputRows(m Model, g Geometry) int
- func LineDown(text string, at int) int
- func LineEnd(text string, at int) int
- func LineStart(text string, at int) int
- func LineUp(text string, at int) int
- func LoopPlan(specs []protocol.SpecFolder, t Strings) string
- func LoopTask(spec LoopArgs) string
- func MaxScroll(total, height int) int
- func OSC52(s string) string
- func PageSize(m Model, g Geometry) int
- func Pick(ctx context.Context, choices []SessionChoice, geo Geometry, lang Lang) (string, error)
- func PlanText(m Model) string
- func Render(m Model, g Geometry) string
- func RenderPicker(p Picker, geo Geometry) string
- func RenderStatusBar(m Model, g Geometry) string
- func ReplanPrompt(what string) string
- func Run(ctx context.Context, opts Options) error
- func ScrollHint(m Model, g Geometry, top, total, height int) string
- func SessionList(list []protocol.Session, current string) string
- func ShadowedBuiltins(user config.CommandSet) []string
- func Spinner(frame int, unicode bool) string
- func StreamLines(m Model, g Geometry) []string
- func Window(m Model, g Geometry, body []string) (visible []string, top, total, height int)
- type Builtin
- type CommandKind
- type Completion
- type CopyState
- type Depth
- type Entry
- type FileRow
- type FileState
- type Geometry
- type Kind
- type Lane
- type Lang
- type LoopArgs
- type Model
- func (m Model) AcceptCompletion() Model
- func (m Model) Apply(ev protocol.Event) Model
- func (m Model) Backspace() Model
- func (m Model) CloseCompletions() Model
- func (m Model) DeleteForward() Model
- func (m Model) DeleteWord() Model
- func (m Model) DrainQueue() (Model, string)
- func (m Model) Enqueue(text string, max int) (Model, bool)
- func (m Model) EnsureCursorVisible(g Geometry) Model
- func (m Model) EnterCopy(lastLine int) Model
- func (m Model) ExtendCopy(delta, lastLine int) Model
- func (m Model) HistoryNext() Model
- func (m Model) HistoryPrev() Model
- func (m Model) Insert(s string) Model
- func (m Model) LeaveCopy() Model
- func (m Model) MoveCompletion(delta int) Model
- func (m Model) PlanCounts() (done, total, blocked int)
- func (m Model) PlanSummary() string
- func (m Model) Refresh(user config.CommandSet) Model
- func (m Model) Remember(text string) Model
- func (m Model) RemoveFromQueue(i int) Model
- func (m Model) ScrollBy(lines int, g Geometry) Model
- func (m Model) ScrollToBottom(g Geometry) Model
- func (m Model) ScrollToTop() Model
- func (m Model) SetInput(s string) Model
- func (m Model) ShowEmptyState() bool
- func (m Model) ToggleAt(i int) Model
- type Options
- type Palette
- type Picker
- type RailMode
- type RailNav
- func (n RailNav) Backspace() RailNav
- func (n RailNav) BackspaceName() RailNav
- func (n RailNav) Chosen(all []SessionChoice) string
- func (n RailNav) Escape() RailNav
- func (n RailNav) Matches(c SessionChoice) bool
- func (n RailNav) Move(d, n_ int) RailNav
- func (n RailNav) StartNaming(all []SessionChoice) RailNav
- func (n RailNav) Type(r string, all []SessionChoice) RailNav
- func (n RailNav) TypeName(r string) RailNav
- func (n RailNav) Visible(all []SessionChoice) []SessionChoice
- type Resolved
- type SessionChoice
- type Strings
- type Style
- type Theme
- type Transport
- type UpdateResult
Constants ¶
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.
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.
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.
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.
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 ¶
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.
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.
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
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
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 BodyHeight ¶
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 ¶
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 ContextLabel ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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 LineStart ¶
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 ¶
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
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 OSC52 ¶
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 ¶
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 ¶
Pick asks which conversation to continue and returns its id, empty when the person chose none.
func Render ¶
Render draws the whole screen. Pure over model and geometry, which is what allows exact golden tests with no TTY anywhere.
func RenderPicker ¶
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 ¶
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 Run ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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.
type Depth ¶ added in v0.5.0
type Depth int
Depth is how much colour the terminal can take.
func ColorDepth ¶ added in v0.5.0
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
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.
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 ¶
DefaultGeometry returns the documented defaults.
func (Geometry) ShowRail ¶ added in v0.2.0
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 ¶
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.
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
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 RailNav
//
// 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 ¶
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 ¶
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 ¶
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) CloseCompletions ¶
CloseCompletions hides the menu until the line changes.
func (Model) DeleteForward ¶
DeleteForward deletes under the caret.
func (Model) DeleteWord ¶
DeleteWord removes the word before the caret, trailing spaces included.
func (Model) DrainQueue ¶
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 ¶
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 ¶
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) ExtendCopy ¶
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 ¶
HistoryNext walks forward, and past the newest entry returns the draft.
func (Model) HistoryPrev ¶
HistoryPrev walks back through what was sent.
func (Model) MoveCompletion ¶
MoveCompletion walks the menu, wrapping at both ends.
func (Model) PlanCounts ¶
PlanCounts returns done, total and blocked.
func (Model) PlanSummary ¶
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 ¶
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 ¶
RemoveFromQueue drops a queued message before it is sent.
func (Model) ScrollBy ¶
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 ¶
ScrollToBottom jumps to the newest output and resumes following.
func (Model) ScrollToTop ¶
ScrollToTop jumps to the beginning of the session.
func (Model) ShowEmptyState ¶
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.
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.
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.
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 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.
// 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.
}
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
Backspace widens it, and drops the last rune rather than the last byte.
func (RailNav) BackspaceName ¶ added in v0.2.0
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
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
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) 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.
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
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 ¶
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 ¶
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 VerificationLabel ¶
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.
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.