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 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 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 Entry
- type FileRow
- type FileState
- type Geometry
- type Kind
- type Lang
- 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 PanelMode
- 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 Transport
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: "resume"}, {Name: "undo"}, {Name: "image"},
}
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 ¶
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.
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 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 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 )
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 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
// 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
// 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 is the line count the tool reported, never parsed back out of its
// summary.
Added 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
PanelWidth int
PanelMinWidth int
PanelMaxWidth int
PanelMinTotalWidth int
PanelMode PanelMode
// The sidebar. clamp(20, w/5, 30) wide when it is asked for, and asked for
// is the only way it appears — there is no width rule here any more, and
// RailMode says why.
RailWidth int
RailMinWidth int
RailMaxWidth 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) ShowPanel ¶
ShowPanel reports whether the plan panel is drawn.
Responsive by default: at 80 columns a 24-wide panel leaves 56 for the stream, and a diff in 56 columns is bad. But responsiveness answers the case where the user never noticed the window got narrow — and a keypress *is* the user noticing, so an explicit choice wins over the default at any width.
No plan means no panel in every mode: an empty panel is worse than none.
func (Geometry) ShowRail ¶ added in v0.2.0
ShowRail reports whether the sidebar is drawn.
Nothing to put in it means no sidebar, for the reason an empty panel is worse than none: a column of nothing costs the stream twenty characters and tells the reader that something is missing.
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" // 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 Lang ¶
type Lang string
Lang is a declared interface language.
type Model ¶
type Model struct {
SessionID string
Workspace string
Model string
Sandbox 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
// 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
// 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
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
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
// 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
}
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
PanelMode is how the plan panel decides whether to show. RailMode is the sidebar's visibility, and it mirrors PanelMode deliberately: the same question was answered once already, and answering it a second way would give the two columns different manners on the same terminal.
const ( // RailHidden is where a terminal starts, and the zero value says so. // // There is no third mode here, and the panel next door still has one. That // asymmetry is the point rather than an oversight: the two columns used to // answer "should I be here?" the same way, with the same threshold, and // their two hundreds COMPOUNDED — crossing from 99 to 100 columns cost the // conversation 46 of them at once. Mirroring the panel is what built the // cliff. // // They also hold different things. The panel holds the plan, which exists // only when the model made one and is something a reader returns to. The // column held a second copy of what the stream had just said. A rule that // suits the first does not suit the second, and writing one rule for both // is how it came to be written for neither. RailHidden RailMode = iota // RailShown is the user having asked, and it holds at any width — the same // manners the panel has, and the half of the mirror worth keeping. 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
PanelTurn string
PanelRounds string
PanelInFlight string
RailHidden string
RailFiles string
RailSessions string
RailFilter string
RailNaming string
RailNoMatch 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.
ApprovalTitle string
ApprovalCrosses string
ApprovalNetwork string
ApprovalStanding string
ApprovalOnce string
ApprovalEnter 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
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
// 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 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 Transport ¶
type Transport interface {
CreateSession(ctx context.Context, req protocol.CreateSessionRequest) (protocol.Session, error)
ListSessions(ctx context.Context) ([]protocol.Session, 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
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
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.