Documentation
¶
Overview ¶
Package tui implements the kata terminal UI built on Bubble Tea.
Index ¶
- func Run(ctx context.Context, opts Options) error
- type APIError
- type ChildCounts
- type Client
- func (c *Client) AddComment(ctx context.Context, projectID int64, ref, body, actor string) (*MutationResp, error)
- func (c *Client) AddLabel(ctx context.Context, projectID int64, ref, label, actor string) (*MutationResp, error)
- func (c *Client) AddLink(ctx context.Context, projectID int64, ref string, body LinkBody, actor string) (*MutationResp, error)
- func (c *Client) Assign(ctx context.Context, projectID int64, ref, owner, actor string) (*MutationResp, error)
- func (c *Client) Close(ctx context.Context, projectID int64, ref, actor string) (*MutationResp, error)
- func (c *Client) CreateIssue(ctx context.Context, projectID int64, body CreateIssueBody) (*MutationResp, error)
- func (c *Client) EditBody(ctx context.Context, projectID int64, ref, body, actor string) (*MutationResp, error)
- func (c *Client) GetIssueDetail(ctx context.Context, projectID int64, ref string) (*IssueDetail, error)
- func (c *Client) ListAllIssues(ctx context.Context, f ListFilter) ([]Issue, error)
- func (c *Client) ListComments(ctx context.Context, projectID int64, ref string) ([]CommentEntry, error)
- func (c *Client) ListEvents(ctx context.Context, projectID int64, ref string) ([]EventLogEntry, error)
- func (c *Client) ListIssues(ctx context.Context, projectID int64, f ListFilter) ([]Issue, error)
- func (c *Client) ListLabels(ctx context.Context, projectID int64) ([]LabelCount, error)
- func (c *Client) ListLinks(ctx context.Context, projectID int64, ref string) ([]LinkEntry, error)
- func (c *Client) ListProjects(ctx context.Context) ([]ProjectSummary, error)
- func (c *Client) ListProjectsWithStats(ctx context.Context) ([]ProjectSummaryWithStats, error)
- func (c *Client) RemoveLabel(ctx context.Context, projectID int64, ref, label, actor string) (*MutationResp, error)
- func (c *Client) RemoveLink(ctx context.Context, projectID int64, ref string, linkID int64, actor string) (*MutationResp, error)
- func (c *Client) Reopen(ctx context.Context, projectID int64, ref, actor string) (*MutationResp, error)
- func (c *Client) ResolveProject(ctx context.Context, startPath string) (*ResolveResp, error)
- func (c *Client) SetPriority(ctx context.Context, projectID int64, ref string, priority *int64, ...) (*MutationResp, error)
- type CommentEntry
- type CreateInitialLinkBody
- type CreateIssueBody
- type EventEnvelope
- type EventLogEntry
- type Issue
- type IssueDetail
- type IssueRef
- type KataAPI
- type LabelCount
- type LinkBody
- type LinkEntry
- type LinkPeer
- type ListFilter
- type Model
- type MutationResp
- type Options
- type ProjectStatsSummary
- type ProjectSummary
- type ProjectSummaryWithStats
- type ResolveResp
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type APIError ¶
APIError is the structured form of the §4.6 error envelope.
type ChildCounts ¶
ChildCounts is the direct-child aggregate attached to queue/detail rows.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the typed adapter the TUI uses to talk to the daemon. Errors include the request method+path so toast messages stay actionable.
func NewClient ¶
NewClient wraps a pre-built *http.Client with a typed daemon adapter. base is the daemon URL — "http://kata.invalid" for unix-socket transport.
func (*Client) AddComment ¶
func (c *Client) AddComment( ctx context.Context, projectID int64, ref, body, actor string, ) (*MutationResp, error)
AddComment appends a new comment to the issue.
func (*Client) AddLabel ¶
func (c *Client) AddLabel( ctx context.Context, projectID int64, ref, label, actor string, ) (*MutationResp, error)
AddLabel attaches a label to the issue.
func (*Client) AddLink ¶
func (c *Client) AddLink( ctx context.Context, projectID int64, ref string, body LinkBody, actor string, ) (*MutationResp, error)
AddLink creates a typed link from this issue to body.ToRef. The daemon's CreateLinkRequest.Body is {actor, type, to_ref}; ToRef accepts a short_id, qualified short_id ("kata#abc4"), or a 26-char ULID.
func (*Client) Assign ¶
func (c *Client) Assign( ctx context.Context, projectID int64, ref, owner, actor string, ) (*MutationResp, error)
Assign sets the issue owner. Empty owner routes to /actions/unassign because the daemon's PATCH endpoint cannot represent the clear case (string vs null) and /actions/assign rejects empty owners with 400.
func (*Client) Close ¶
func (c *Client) Close( ctx context.Context, projectID int64, ref, actor string, ) (*MutationResp, error)
Close transitions the issue to status=closed. source=tui signals the daemon to skip the substance and evidence checks that gate CLI-side closes; the structural guards (parent-close completeness, sibling throttle, repeated-message) still apply.
func (*Client) CreateIssue ¶
func (c *Client) CreateIssue( ctx context.Context, projectID int64, body CreateIssueBody, ) (*MutationResp, error)
CreateIssue posts a new issue. body.IdempotencyKey rides the Idempotency-Key header per spec §4.4 when non-empty.
func (*Client) EditBody ¶
func (c *Client) EditBody( ctx context.Context, projectID int64, ref, body, actor string, ) (*MutationResp, error)
EditBody replaces issue.body via PATCH. v1 only supports body edits from the TUI; title edits would reuse the same endpoint.
func (*Client) GetIssueDetail ¶
func (c *Client) GetIssueDetail(ctx context.Context, projectID int64, ref string) (*IssueDetail, error)
GetIssueDetail fetches a single issue plus hierarchy metadata by ref. ref is a short_id, qualified short_id, or UID — the daemon's path resolver picks the matching column.
func (*Client) ListAllIssues ¶
ListAllIssues lists issues across every project. The daemon may not yet implement /api/v1/issues; in that case the request surfaces as a 404 APIError that callers can downgrade.
func (*Client) ListComments ¶
func (c *Client) ListComments( ctx context.Context, projectID int64, ref string, ) ([]CommentEntry, error)
ListComments and ListLinks route through GET /issues/{ref} because the daemon embeds both slices there. ListEvents filters client-side because the poll endpoint accepts no issue-targeted query filter.
func (*Client) ListEvents ¶
func (c *Client) ListEvents(ctx context.Context, projectID int64, ref string) ([]EventLogEntry, error)
ListEvents returns the events tab data for one issue. See above note on the client-side filter. ref is the issue's short_id; the daemon's event stream embeds issue_short_id on every issue-scoped event, so the filter is project-local and stable for the life of the daemon.
TODO(plan-6/task-8): the 200-event window is a one-shot snapshot; full pagination via next_after_id is deferred. The poll envelope's reset_required is decoded but ignored here because Task 8 fetches once per detail-view open; the SSE consumer (Task 11) handles reset_required for the long-lived stream.
func (*Client) ListIssues ¶
ListIssues returns the issues for projectID filtered by f.
func (*Client) ListLabels ¶
ListLabels returns the per-label aggregate counts for projectID. The daemon's GET /api/v1/projects/{id}/labels endpoint backs the + suggestion menu; counts drive the "most-used first" sort.
func (*Client) ListProjects ¶
func (c *Client) ListProjects(ctx context.Context) ([]ProjectSummary, error)
ListProjects returns the daemon's known projects.
func (*Client) ListProjectsWithStats ¶
func (c *Client) ListProjectsWithStats(ctx context.Context) ([]ProjectSummaryWithStats, error)
ListProjectsWithStats returns every active project with per-project aggregates {open, closed, last_event_at} populated. Used by the projects view. Spec §7.3.
func (*Client) RemoveLabel ¶
func (c *Client) RemoveLabel( ctx context.Context, projectID int64, ref, label, actor string, ) (*MutationResp, error)
RemoveLabel sends actor in the query string because DELETE bodies are non-portable; the label is path-escaped to survive '/' and similar.
func (*Client) RemoveLink ¶
func (c *Client) RemoveLink( ctx context.Context, projectID int64, ref string, linkID int64, actor string, ) (*MutationResp, error)
RemoveLink deletes a link by id. actor rides the query string per the DELETE-body portability convention.
func (*Client) Reopen ¶
func (c *Client) Reopen( ctx context.Context, projectID int64, ref, actor string, ) (*MutationResp, error)
Reopen transitions the issue back to status=open.
func (*Client) ResolveProject ¶
ResolveProject runs the §4.2 resolution flow against startPath.
Wire shape is chosen client-side so the daemon never has to stat the client's filesystem (issue #35): {name, alias?} when .kata.toml is readable, {alias} for a git workspace without .kata.toml, and {start_path} as a legacy local-only fallback. When the daemon returns a canonical name that differs from the local .kata.toml, the client rewrites the file in place.
func (*Client) SetPriority ¶
func (c *Client) SetPriority( ctx context.Context, projectID int64, ref string, priority *int64, actor string, ) (*MutationResp, error)
SetPriority sends the issue's priority through /actions/priority. A nil priority clears the field. Mirrors Assign's pattern of routing the optional/clear case through the same endpoint with a nil body field; the daemon distinguishes set-vs-clear from the JSON shape.
type CommentEntry ¶
type CommentEntry struct {
ID int64 `json:"id"`
Author string `json:"author"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
}
CommentEntry is the per-comment projection rendered in the comments tab.
type CreateInitialLinkBody ¶
type CreateInitialLinkBody struct {
Type string `json:"type"`
ToRef string `json:"to_ref"`
Incoming bool `json:"incoming,omitempty"`
}
CreateInitialLinkBody requests a link created atomically with a new issue. ToRef is a short_id, qualified short_id ("kata#abc4"), or a 26-char ULID; the daemon resolves it to the target issue at request time.
Incoming reverses direction for type=blocks: when true, the new issue is the link's "to" side (i.e. the new issue is BLOCKED BY the named target — the `kata create --blocked-by N` shape). Default (false) leaves the new issue as the link's "from" side (`--blocks N`). Rejected by the daemon for type=parent (no inverse parent direction is exposed) and meaningless for type=related (which is symmetric).
type CreateIssueBody ¶
type CreateIssueBody struct {
Title string `json:"title"`
Body string `json:"body,omitempty"`
Actor string `json:"actor"`
Owner *string `json:"owner,omitempty"`
Labels []string `json:"labels,omitempty"`
Links []CreateInitialLinkBody `json:"links,omitempty"`
ForceNew bool `json:"force_new,omitempty"`
IdempotencyKey string `json:"-"`
}
CreateIssueBody is the input to CreateIssue. IdempotencyKey rides the Idempotency-Key header per spec §4.4 instead of the JSON body.
Owner and Labels are populated by the new-issue form (Plan 8 commit 4) when the user fills in the optional fields; they are omitted from the payload when zero so an inline-row commit does not promise the daemon fields it has no value for.
type EventEnvelope ¶
type EventEnvelope struct {
ID int64 `json:"id"`
Type string `json:"type"`
ProjectUID string `json:"project_uid,omitempty"`
IssueUID string `json:"issue_uid,omitempty"`
IssueShortID *string `json:"issue_short_id,omitempty"`
RelatedIssueUID string `json:"related_issue_uid,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
EventEnvelope is the minimal event projection embedded in mutation responses. The richer poll/SSE shape uses EventLogEntry.
type EventLogEntry ¶
type EventLogEntry struct {
ID int64 `json:"event_id"`
Type string `json:"type"`
Actor string `json:"actor"`
ProjectUID string `json:"project_uid,omitempty"`
IssueUID string `json:"issue_uid,omitempty"`
IssueShortID *string `json:"issue_short_id,omitempty"`
RelatedIssueUID string `json:"related_issue_uid,omitempty"`
RelatedIssueShortID *string `json:"related_issue_short_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
Payload map[string]any `json:"payload,omitempty"`
}
EventLogEntry is the per-event projection used by the events tab. IssueShortID is the project-scoped display ref for the subject issue; IssueUID is the canonical reference and survives short_id cutovers.
type Issue ¶
type Issue struct {
ID int64 `json:"id"`
UID string `json:"uid,omitempty"`
ProjectID int64 `json:"project_id"`
ProjectUID string `json:"project_uid,omitempty"`
ShortID string `json:"short_id"`
QualifiedID string `json:"qualified_id,omitempty"`
Title string `json:"title"`
Body string `json:"body"`
Status string `json:"status"`
ClosedReason *string `json:"closed_reason,omitempty"`
Owner *string `json:"owner,omitempty"`
Author string `json:"author"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ClosedAt *time.Time `json:"closed_at,omitempty"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
Labels []string `json:"labels,omitempty"`
ParentShortID *string `json:"parent_short_id,omitempty"`
ChildCounts *ChildCounts `json:"child_counts,omitempty"`
Blocks []LinkPeer `json:"blocks,omitempty"`
BlockedBy []LinkPeer `json:"blocked_by,omitempty"`
Related []LinkPeer `json:"related,omitempty"`
Priority *int64 `json:"priority,omitempty"`
}
Issue is a strict subset of the daemon's wire shape. Labels rides on list-row decode (the daemon embeds them per row) and on a manual copy from showIssue's body.labels for detail open; the omitempty tag keeps absence on a show response from blanking a previously-populated slice. The deleted bool is derived from DeletedAt being non-nil.
UID is the canonical reference; ShortID is the per-project display snapshot (4+ chars of the ULID suffix, per spec §4); QualifiedID is the human-facing "<project>#<short_id>" form populated on list rows (api.IssueOut.QualifiedID).
type IssueDetail ¶
IssueDetail is the hydrated detail payload used by the TUI detail view.
type IssueRef ¶
type IssueRef struct {
UID string `json:"uid"`
ShortID string `json:"short_id"`
QualifiedID string `json:"qualified_id"`
Title string `json:"title"`
Status string `json:"status"`
}
IssueRef is the compact parent issue projection on detail responses. Mirrors api.IssueRef: UID is canonical, ShortID and QualifiedID are display projections rendered at the API boundary.
type KataAPI ¶
type KataAPI interface {
ListProjects(ctx context.Context) ([]ProjectSummary, error)
ListProjectsWithStats(ctx context.Context) ([]ProjectSummaryWithStats, error)
ListLabels(ctx context.Context, projectID int64) ([]LabelCount, error)
ResolveProject(ctx context.Context, startPath string) (*ResolveResp, error)
// contains filtered or unexported methods
}
KataAPI is the daemon surface the TUI consumes. It is owned by this package (the consumer) and covers exactly the methods the TUI calls today — no speculative future-proofing for a remote engine. When that engine lands it will satisfy this interface structurally and the surface can grow alongside concrete need.
The narrower listAPI / detailAPI / labelLister interfaces continue to type the call sites that only need a slice of the surface. KataAPI is the union held by Model.api so a single value can be passed to those narrower seams.
type LabelCount ¶
LabelCount mirrors the daemon's LabelsListResponse.Body.Labels wire shape (db.LabelCount). Local definition keeps the TUI free of an internal/db import — the package boundary stays at the wire layer.
type LinkBody ¶
LinkBody is the request projection for POST /links. ToRef accepts any shape the daemon's resolver understands (short_id, qualified, or 26-char ULID).
type LinkEntry ¶
type LinkEntry struct {
ID int64 `json:"id"`
Type string `json:"type"`
From LinkPeer `json:"from"`
To LinkPeer `json:"to"`
Author string `json:"author"`
CreatedAt time.Time `json:"created_at"`
}
LinkEntry mirrors api.LinkOut: each endpoint is a LinkPeer (UID + short_id) so the TUI can correlate either by canonical ID or by display snapshot.
type LinkPeer ¶
LinkPeer mirrors api.LinkPeer: the canonical UID plus the rendered short_id snapshot for one end of a link.
type ListFilter ¶
ListFilter is the union of filters used by list views. Limit is sent on the wire for capped working-set fetches. Status/Owner/Author/ Labels/Search are applied client-side after the daemon returns results.
IncludeDeleted is deliberately absent: the daemon's list endpoint hard-codes deleted_at IS NULL (internal/db/queries.go::ListIssues) and has no include_deleted query param, so a client-side flag would promise something the wire cannot deliver. Re-introducing it requires daemon work and is deferred to a future task.
type Model ¶
type Model struct {
// contains filtered or unexported fields
}
Model is the top-level Bubble Tea model. Sub-views are embedded by value so Update can mutate them in place without indirection. The detail sub-view is held by value (not pointer) so its scroll/tab state lives across opens of the same issue, and so popDetailMsg returns to a list whose cursor and filters are unchanged.
SSE state lives on the parent model so the consumer goroutine has a fixed channel to push into and the detail/list sub-views can route invalidation. sseCh bridges the long-lived goroutine into the TEA loop via waitForSSE; sseStatus drives the status-bar reconnect indicator; pendingRefetch coalesces bursts of events into a single 150ms-debounced list refetch; cache holds the current list snapshot so a stale-mark + clean refetch can short-circuit redundant work.
toastNow is a clock injection point: production uses time.Now, tests replace it to drive deterministic toast expiry.
func (Model) Init ¶
Init dispatches the initial fetch unless boot landed on the empty state or no client is wired (the latter happens in unit tests that drive the model directly via teatest.NewTestModel and feed initialFetchMsg by hand). The list view sets loading=true at construction so the spinner shows until initialFetchMsg arrives.
waitForSSE is registered alongside fetchInitial so the SSE goroutine (spawned by Run after this Init returns) has a reader the moment its first frame is ready. The reader is replenished on every SSE message in Update so the channel is continuously drained.
func (Model) Update ¶
Update routes messages to the active sub-view. Quit is handled at the top level so it works from every view, EXCEPT while a list-view inline prompt or a detail-view modal is active: typing 'q' into a prompt or modal must reach the buffer instead of quitting. The same gate applies to ?, R, and any future global key.
openDetailMsg / popDetailMsg are intercepted before the per-view dispatch because the view switch lives at this level. The detail sub-model is reset on open so a new issue starts at scroll=0 with the comments tab — but the list sub-model is untouched on pop, preserving the user's cursor and filter state across the round trip.
func (Model) View ¶
View returns the rendered string for the active sub-view. The list view consumes its own SSE state + toast inline (via the M1 chrome); other views still get the SSE/toast extras appended below since they don't carry a status line of their own. Both extras render as empty strings in the steady state so the view does not gain spurious blank lines.
type MutationResp ¶
type MutationResp struct {
Issue *Issue `json:"issue"`
Event *EventEnvelope `json:"event,omitempty"`
Changed bool `json:"changed"`
Reused bool `json:"reused,omitempty"`
}
MutationResp mirrors the §4.5 mutation envelope.
type Options ¶
type Options struct {
Stdout io.Writer // typically os.Stdout
Stderr io.Writer // typically os.Stderr
DisplayUIDFormat string // none, short, or full
Mouse bool // opt-in mouse capture and mouse-driven navigation
}
Options controls TUI behavior. Stable across versions; new fields must be optional.
IncludeDeleted is intentionally absent: the daemon's ListIssuesRequest (internal/api/types.go) does not accept include_deleted, and db.ListIssues hard-codes deleted_at IS NULL, so there is no way for the TUI to surface soft-deleted rows today. Re-introducing the flag is deferred to a follow-up that adds wire + handler support.
AllProjects is intentionally absent from Options: the boot flow always starts in single-project mode (resolved from the cwd) or empty state, and users toggle to all-projects via the R binding at runtime. Adding a CLI flag is reasonable as a future ergonomic but isn't required for the navigation surface.
type ProjectStatsSummary ¶
type ProjectStatsSummary struct {
Open int `json:"open"`
Closed int `json:"closed"`
LastEventAt *time.Time `json:"last_event_at"`
}
ProjectStatsSummary is the per-project aggregate carried by /api/v1/projects?include=stats. LastEventAt is nil for a project with zero events. Spec §7.2.
type ProjectSummary ¶
ProjectSummary is one row of GET /projects.
type ProjectSummaryWithStats ¶
type ProjectSummaryWithStats struct {
ProjectSummary
Stats *ProjectStatsSummary `json:"stats,omitempty"`
}
ProjectSummaryWithStats extends ProjectSummary with the stats triple. The boot project-name cache uses ProjectSummary; viewProjects uses this shape.
type ResolveResp ¶
type ResolveResp struct {
Project struct {
ID int64 `json:"id"`
Name string `json:"name"`
} `json:"project"`
WorkspaceRoot string `json:"workspace_root"`
}
ResolveResp is the body of POST /projects/resolve.
Source Files
¶
- api.go
- cache.go
- client.go
- client_types.go
- detail.go
- detail_event.go
- detail_fetch.go
- detail_mutation.go
- detail_render.go
- detail_tabs.go
- editor.go
- events_sse.go
- events_sse_parse.go
- footer_hints.go
- help.go
- input.go
- inputs_render.go
- keymap.go
- label_cache.go
- layout.go
- list.go
- list_render.go
- markdown_render.go
- messages.go
- model.go
- mouse.go
- projects_view.go
- projects_view_render.go
- queue_rows.go
- quit_modal.go
- run.go
- sanitize.go
- scope.go
- split_render.go
- suggest_render.go
- theme.go
- version.go