AI is disabled in this build. Any AI-mode layouts or controls described below
are retained design reference, not an available workflow. ctrl+a displays
an unavailable notice; the AI settings controls are disabled.
TOP BANNER (no border, 4 rows incl. dashed rule)
┌ Resources ──[n/m]┐┌ <Resource> · <ns> ──────[ zoom ]┐┌ Actions ─┐
│ groups ││ 1 table row ││ [d] … │
│ ▸ item n ││ ▌ 2 selected row ││ ─────── │
│ ││ 3 … OR text view ││ [D] Del │
└──────────────────┘└─────────────────────────────────┘└──────────┘
┌ Command/Prompt ────────────────────[ CMD | AI · model ]┐
│ ❯ or ✦ input │
└────────────────────────────────────────────────────────┘
status bar (toast · key hints)
Geometry (layout()): banner 4 rows, prompt 3, status 1, middle gets the
rest. Left/right panes are 22/24 cols (18/20 under 96 cols; 0 when zoomed).
Below 72×22 the UI is replaced by a "terminal too small" notice.
Neither side pane spends rows on a permanent search box — see Search boxes below.
Row 1: ⎈ k10s │ context │ ver │ nodes 2/3 ready … ns <name> ▾ │ theme <name> ⟳
Identity on the left, two buttons on the right:
- the node counter turns warn-colored when any node is NotReady;
ns <name> ▾opens the Namespaces table in the main panel, whose row0isall(real namespaces are then1..N, matching the sidebar count).enterthere switches namespace and returns to whichever kind you came from — Services if you were looking at Services.:nsdoes the same;theme <name> ⟳opens the theme picker (the same one/themeshows).
Row 3: cluster totals — CPU and MEM gauges (average of per-node usage from
metrics-server). Per-node detail lives in Resources ▸ Nodes, not the banner.
Gauges color by threshold: ok < 60% ≤ warn < 85% ≤ err. A trend arrow
follows each percentage: red ▲ when usage just rose, green ▼ when it
fell. It stays for about half a minute of unchanged readings (trendTicks
in trend.go) and then clears. The same arrows sit at the end of the
CPU/MEM cells in the Pods table and CPU%/MEM% in Nodes, tracked per
row, so a pod that just spiked stands out without reading every number.
30 resource kinds grouped Workloads / Network / Config / Storage / RBAC /
Cluster / Custom Resources, each with a row count for the current namespace.
Selected row: ▸ + accent. :po, :deploy, :pv … jump straight to one —
see commands.md.
The wheel scrolls this pane, but scrolling never changes the selection — it is looking, not picking, so the main panel stays on the kind you chose. That distinction is why the pane used to refuse the wheel outright: when scrolling was selecting, brushing the wheel on the way to the table swapped out the whole view.
↑/↓ in the panel title say which way there is more. Moving the selection
with the arrow keys drags the window along just far enough to keep it
visible, its group header included — selection moves the scroll, never the
other way round.
Thirty kinds do not fit a laptop-sized sidebar, so the groups people consult
occasionally — Config, Storage and RBAC — start folded, and the choice
persists (collapsed in config.md).
space (list focused) |
fold / unfold the group you are standing in |
left |
fold it |
click a ▾/▸ header |
either |
A folded header states how many kinds it hides, and keeps the ▸ marker if
the kind currently open is one of them — folding must never turn "where am
I" into a guess. Arrow keys walk only what is on screen and never open a
group you folded; arriving by :sec, ctrl+p or a search does unfold the
group it lands in, since the alternative is a table whose sidebar row is
invisible. A search ignores folding entirely: a match hidden behind a
fold would make the filter look broken.
Folding is not only tidiness. The sidebar is what tells the backend which kinds to count, so a folded group stops sending badge-count requests altogether — see performance.md.
Counts. The live backend only watches kinds you have opened, so counts
for the rest come from a cheap background sweep (one limit=1 request per
kind, reading remainingItemCount). A kind whose count isn't known yet shows
no badge rather than a misleading 0, and a count already on screen is kept
while a newly-opened kind syncs. See performance.md.
Filtering. tab to this pane and type: every printable key filters by
name / short name / group, case-insensitive; ↑↓ moves, enter/→ returns
to the table, esc clears. :search <term> and ctrl+p do the same without
focusing. The active filter shows in the panel title (Resources · po)
and the match counter in the top-right tag (3/30), alongside the ↑/↓
scroll hints. The selection snaps to the first match so the centre table
follows.
Two modes:
- Table — a left gutter carries the selection marker
▌and a dim 1-based row number, sized to the largest number present. Remaining columns are auto-sized; when space runs out, widths shrink to per-column minimums (first column min 18, NAMESPACE min 9) and then whole columns drop from the right. Cell colors by status (CrashLoopBackOfferr,Pendingwarn,Runningok,x/ymismatch warn); a NAMESPACE column renders in accent2. Selected row: selection bg, bold on the identity column (NAME, or OBJECT for Events — found by header name, not a fixed index, since that column shifts right under:ns all). - Text — describe / YAML / help render in place with a
[ close ]tag and a bottom scroll bar (n/total % hints). - Logs — its own viewer; see below.
[ zoom ] (or z) hides both side panes; [ restore ] / z / esc undoes.
The choice is saved (zoomed in config.md), so a restart opens
zoomed if you left it that way.
Rows are sorted A→Z by name by default — informer caches return objects in
arbitrary order, so without this the table would reshuffle on every refresh.
Sorting is natural: pod-2 comes before pod-10. Under :ns all rows
group by namespace first, then name.
Events are the exception and stay newest-first; sorting them by their
first column (TYPE) would bury the newest behind every Normal event.
s opens an interactive shell in the main panel, not by handing the
whole terminal over: the resource list, header and status bar stay visible
while you are inside the pod. k10s runs a small terminal emulator (vt10x)
over the exec stream, feeds it the pod's output, and renders its screen into
the pane; the emulator is sized to the pane and resizes with it.
Every keystroke goes to the pod — q, /, esc and the action hotkeys all
belong to the program you are running. ctrl+] detaches (the telnet and
docker convention, chosen because nothing inside a shell wants it), as does
the [ detach ] button. When the shell exits on its own the panel returns
to the table.
Workloads resolve to one of their pods, the same way logs do. Backends without a shell (the offline demo) say so in the status bar rather than failing.
:ctx lists kubeconfig contexts in the main panel, numbered like a
table and marking the active one current. enter reconnects. Same idea as
the namespace chooser: full width beats a cramped popup for names this long.
The list always ends up holding one entry that is not a cluster:
k10s-demo, the built-in demo. It is labelled on its own row
(k10s demo · sample data) and explained in a legend under the list, because
a context list is exactly where someone decides what they are looking at, and
a name alone would not tell them. Picking any other context leaves the
demo — that is the only exit, and it is the same gesture as any other
switch. The list stays complete inside the demo: kubeconfig's contexts are
merged in from the startup read, so the way out is always on screen.
Any action that has to wait — describe, YAML, logs, top, AI, and the
mutating ones — takes over the main panel with a spinner naming what is
running. This covers enter and double-click as well, and matters most for
actions whose only result is a toast (port-forward, cordon), where silence
used to look like nothing had happened.
Opened with l, enter or a double-click on anything that has logs.
- Newest at the bottom, and the view opens pinned there.
- Line numbers count up from the bottom: the newest line is always
1, so a number keeps meaning the same thing as new lines arrive. - Opens at the bottom already, with the newest 500 lines and no loading flash — the view is where you want it from the first frame.
- Following: new lines append at the bottom while pinned. Scrolling up
pauses ("⏸ paused"), and returning to the bottom — or pressing
End— resumes ("● following"). While paused the view holds still even as new lines arrive underneath. - Scroll-back is endless: reaching the oldest loaded line pulls the next
500 older ones, then the next 500, and so on. The Kubernetes log API has no backwards cursor, so
"older" means re-reading with a larger tail; the status line says
↑ for olderuntil the beginning is reached, thenstart of log. - Lines wrap instead of being cut off with an ellipsis — a truncated log line is often exactly the part you needed. Continuation rows have no number.
- Level tokens are coloured (
ERROR/FATALred,WARNamber,INFOgreen,DEBUGdim) and the leading timestamp is dimmed; the message itself stays in the normal foreground, because that is what you actually read.
Kinds with no logs of their own fall back to describe rather than reporting an error — there is nothing the user could do about "this kind has no logs". Workloads (Deployment, ReplicaSet, StatefulSet, DaemonSet, Job) resolve to one of their pods, preferring a running one.
While a kind's informer is still doing its first list, the pane shows a centred spinner, an indeterminate bar and a one-line explanation — never "no resources found", which would be a lie. The repaint tick runs faster (150ms) while loading and backs off to 2s once synced.
When the list or watch fails, or a watch the server closed is not re-established within a few seconds, an empty pane shows that error in place of the spinner. Rows already cached stay on screen; the error clears once the watch is back.
While the demo backend is on screen the header shows its context in the warn
colour plus a DEMO sample data · :ctx to leave marker — on every frame, not
in a toast that scrolls away, because every number and gauge beside it is
sample data. k10s demo (shell), /demo (anywhere) and :ctx all reach it;
picking another context leaves it. See
backends.md.
When the connection settles with nothing behind it — no kubeconfig, or a
context whose API server does not answer — the main panel becomes a
warn-bordered No cluster panel: the reason client-go gave, what k10s
reads ($KUBECONFIG, else ~/.kube/config), three numbered steps with
links, and the two commands that check the connection from outside k10s.
r retries, :ctx picks another context, /setup opens the long guide
(cluster-setup.md).
Nothing on the frame is invented in that state: no rows, no sidebar badges,
ver — and nodes — in the header instead of a version and a 0/0 ready,
and no CPU/MEM totals — those are computed against a per-node capacity
constant, so with no nodes they would be a capacity figure for a cluster
that is not there. The Actions pane says "nothing to act on" rather than
listing buttons that can only produce errors.
This is why the demo backend is opt-in (k10s demo). It used to be the
fallback here, which meant a machine with no cluster showed a full one.
A failed :ctx switch is a different case and stays on the cluster you have:
losing a working session because another cluster is down helps nobody.
Neither pane keeps a search box on screen permanently:
- Resources pane — no box at all. It is type-to-filter, and the filter state lives in the title/tag.
- Main pane — the row-search box appears only while it's in use (focused,
or holding a filter), so a table normally gets the full panel height. Open
it with
f— advertised as a[ f to search ]tag beside[ zoom ]— and the active filter also shows in the panel title.
Header shows the current target (po/name). The pane lists only actions
that apply to the selected kind — no dimmed rows. Risky actions (Drain,
Delete) sit below a rule and are drawn in the error color. With no cluster
it lists nothing at all — see No cluster.
Notable behavior:
- Logs (
l) follows the stream live, likekubectl logs -f. - Shell (
s) opens a real interactive exec session inside the main panel — see below. - Port Forward (
p) starts a real forward on an ephemeral local port and toasts the address; pressing it again stops that forward, or cancels it while it is still connecting. - Edit (
e) fetches live YAML into a temp file, hands the terminal to$EDITOR, and applies the result on exit. - Scale (
c) pre-fills the prompt with:scale <current>instead of guessing a number. - Rows light up under the pointer and flash when clicked, so a click is acknowledged even when the action only produces a toast.
- Cordon (
o, nodes only) togglesspec.unschedulable; the label flips between "Cordon" and "Uncordon" to match the node's state, and the STATUS cell gains a warn-colored,SchedulingDisabledsuffix — the same conventionkubectl get nodesuses. - Drain (
u, nodes only) cordons, then evicts every non-DaemonSet, non-mirror pod via the eviction API, reporting any that refused. A pod with no controller blocks the drain before the node is cordoned.
Bordered, mode tag on the right ([ CMD ] / [ AI · model ], clickable),
and a [ grow ] / [ shrink ] button beside it.
Normally one input row. ctrl+z — or simply typing something that is not a
/ or : command — grows it to half the screen and wraps the value across
the rows, because shell lines and AI prompts get long and a sideways-
scrolling one-liner hides most of what you typed. esc shrinks, esc again
leaves.
In CMD mode a line that is not a / or : command runs in your
shell, and its output opens as a text view titled with the command
($ date). It runs off the event loop with a 30-second cap and no terminal
attached, so a hung command costs a spinner and then an error rather than a
frozen UI — and an interactive one (vim, kubectl exec -it) simply times
out; s on a pod is the real shell. See commands.md.
-
Confirm (restart, delete, drain): centered, danger variant gets err-colored border/title; Enter·Confirm / Esc·Cancel buttons are clickable.
-
Search palette (
ctrl+p): one box over kinds and objects, each hit labelled with where it lives; the footer names how many kinds are matched by name only. (Contexts and namespaces are not overlays — both list in the main panel.) -
Theme picker (
/theme): each row carries a swatch of that theme's own colors, and moving the cursor applies the theme immediately so you see it before committing.escrestores whatever was active before. -
Settings (
/settings): one dialog with the command names and the AI provider block (radio, Base URL, Model, masked API Key).↑↓moves,enterticks a name or starts editing a field, clicking a row does the same,tabreaches Save. Text fields show a blinking caret.The built-in command names are stated, not chosen:
kubectl,k8sandkalways work at the prompt, so there is nothing to tick and no way to end up with none enabled. One row adds a name of your own, which then becomes the one shown in hints; clearing it falls back tokubectl. -
Command popup: appears while the input starts with
/(do something) or:(narrow what's on screen) — each prefix lists only its own set.↑↓moves the highlight,enterruns the highlighted command outright,tabcompletes instead, rows are clickable.
The pickers share a Save button reachable with tab.
tab cycles Resources → Main → command box in layout order, and
shift+tab walks it backwards. The prompt can also be entered with /, :
or a click, and left with esc.
The Actions pane is not in the cycle — every action already has a hotkey and a clickable row, so a tab stop there would be a keystroke that leads nowhere. Clicking a resource row or an action acts on it without moving focus.
Clicking blank space in the centre pane focuses it. While a popup is open the wheel belongs to the popup — letting it through meant scrolling a picker quietly moved the table behind it. The side panes respond to clicks (pick a kind, fire an action) but not to the wheel — scrolling the resource list changes the entire view, which was too easy to trigger by accident while reaching for the table.
Double-clicking a table row opens it, the same as enter: logs when the
kind has them, describe otherwise — or, on the Namespaces table, switching to
that namespace and showing its pods.