v0.15.0 is a feature-heavy release. Eight new user-facing capabilities land alongside a substantial correctness/perf audit, with the README and in-app help brought back in sync with the code.
Highlights
- More ways in. Mouse support (wheel scroll, click to select, click a header to sort) and a global fuzzy find (
:find <text>) over object names across all namespaces. - GitOps gets first-class treatment.
:diffnow works on Flux- and Helm-managed objects (session fallback whenlast-applied-configurationis absent), and⏎on aHelmReleasejumps straight into the Helm release history it manages. - Sofka watches while you work.
:notifyattaches a long-lived watch to a single object so you can browse other views and still get a flash + terminal bell + desktop notification on every state change. - Honest answers under error and load. A code-health audit surfaced swallowed read errors (a 403 previously diagnosed as an empty pod set) and added panic safety, frame coalescing, sort-key caching, and viewport windowing so event storms stay smooth.
Added
Mouse support (#117)
- Wheel scrolls every view (table, logs, documents, pickers) by synthesizing the mode's own up/down keys, so mouse navigation can never disagree with keyboard navigation.
- Click a table row to select it; click a column header to sort by that column (click again to flip direction).
- Set
mouse = falsein the config to keep the terminal's native text selection. Mouse capture is released while a suspended command (kubectl exec,$EDITOR) runs, on normal exit, and in the panic hook.
Global fuzzy find (:find <text> / :fd) (#120)
- Lists a curated set of common kinds — workloads, pods, services, configmaps, secrets, ingresses, jobs, cronjobs, PVCs, nodes, namespaces, Kustomizations, HelmReleases — across all namespaces concurrently, fuzzy-matches names, and opens a ranked picker.
⏎on a hit jumps to the object via the existing navigation path (a name-filtered view of its kind), so logs/YAML/events are one keystroke away.- Kinds the user can't list (RBAC) make the result flash "find is incomplete — N kind(s) could not be listed" instead of silently pretending the sweep was exhaustive.
- Bare
:findprints usage. A curated kind set is deliberate — listing every CRD per search would hammer the API server for kinds nobody names things in.
Saved port-forwards with autostart (#119)
-
Port-forwards previously lived only as session children — killed on quit, retyped every morning.
[[forwards]]saves them:name,target(kubectl syntax),namespace,ports, optionalautostartand acontextsallowlist.[[forwards]] name = "argocd" target = "svc/argocd-server" namespace = "argocd" ports = "8080:443" autostart = true # start when sofka connects contexts = ["home"] # optional: only these contexts
-
:pfnow lists running forwards (●) followed by saved-but-stopped entries (○ dimmed);⏎/sstarts a stopped one,x/sstops a running one (⏎on a running forward is deliberately a no-op — a reflexive enter shouldn't kill a tunnel). -
Already-running entries are never duplicated; forwards from the previous context are left alone since kubectl pinned their
--contextat spawn.
Watch notifications (:notify / :bell) (#116)
- Toggle a notification on the selected object — "tell me when this rollout finishes / pod becomes Ready / Kustomization reconciles" and keep working.
- Each notify is its own bounded single-object watch (
fields metadata.name=…), deliberately kept out of the generation-scoped task list so it survives view switches and fires from any view until toggled off or the session ends. - Change detection reuses the timeline's pure
transitions()(rollout progress, readiness, phase, restarts, waiting reasons, conditions), so:notifyandTagree on what counts as a state change. The initial list is suppressed — only changes after you toggled are news. Deletion and (re)creation notify too. - Notifications arrive as a generation-free
Msg::Notify: 🔔 flash in the status line, terminal bell, and an OSC 9 desktop notification (iTerm2/kitty/WezTerm/foot pop a system notification; other terminals ignore the sequence). Session-local, nothing on disk.
Flux HelmRelease → Helm release history (#115)
- sofka already had a full native Helm storage inspector (
:helm→ history → values / manifest / NOTES / rollback), but it was unreachable from theHelmReleaseobjects a Flux user actually browses. ⏎on ahelmreleasesrow now drills straight into thehelmhistoryview for the Helm release that object manages. All existing history-view keys work from there (⏎values,ymanifest,dNOTES,rrollback), andescreturns to the HelmRelease list.- Storage coordinates are resolved exactly the way helm-controller composes them:
spec.releaseName(defaulting to[<targetNamespace>-]<name>) andspec.storageNamespace(defaulting to the object's namespace).
Session diff — :diff works on GitOps-managed objects (#113)
:diffpreviously diffed live vslast-applied-configurationonly — an annotation that is empty for every Flux/Helm-managed object, so the diff view was useless precisely for sofka's GitOps focus.- The watch
Appliedhandler now keeps the previous revision of changed objects (bounded FIFO, 256 entries, keyed by kind + store key; survives view switches) next to the existing timeline observation. - When the annotation is absent,
:diffshows previous → live from the session watch, titled(session: previous → live). When neither baseline exists, it says so explicitly.resourceVersionis stripped from both sides (pure noise).
Node %CPU / %MEM of allocatable (#114)
- The nodes view showed absolute CPU/MEM usage only —
status.allocatablewas never read, so the number a nodes view is actually opened for ("how full is this node") didn't exist. - Nodes now get
%CPU/%MEMcolumns: metrics usage as a percentage of allocatable, rendered57%(or-when allocatable is unknown), colored by the existing configurableutilizationthresholds (green / peach ≥ warn / red ≥ critical). - Both columns sort by value via the sort picker; they're treated as volatile sorts like CPU/MEM (metrics move without a resourceVersion bump).
Condition-typed columns, by name (#118)
- Custom
[views]columns get a newtype = "condition":pathis the condition type name (Ready,Available, …), looked up instatus.conditionsby name — replacing the fragile/status/conditions/0/statusarray-index idiom, since condition order isn't guaranteed by anything. RendersTrue/False/Unknown(<none>when absent), sorts as text, and drives row coloring like astatuscolumn. - CRD
additionalPrinterColumnswith the canonical condition filter (.status.conditions[?(@.type=="Ready")].status— how most CRDs such as cert-manager, Flux, ESO express their READY column) were silently dropped by the JSONPath→Pointer translation. They now become condition columns automatically, so those CRDs finally get their READY column with zero configuration. Genuinely untranslatable filter/wildcard expressions are still skipped.
Fixed
Surfaces swallowed errors, panic safety, and hot-path performance (#112)
A code-health audit fixed correctness issues that were hiding in the render and read paths, and tightened the hot path so a busy watch stream stays smooth.
- Read errors no longer render as confident answers:
explain/gitops/bundleprepend an "evidence incomplete — …" finding when a list fails (a 403 previously diagnosed from an empty pod set);pulse/xrayflash "incomplete" instead of showing healthy-looking zero tiles;fleetrows show the list error instead of "0 unhealthy";gitopsdistinguishes a read failure from a genuine 404; metrics poll failures surface in:infoand clear on the next success; an unreadable kubeconfig reports as an error instead of an empty context picker. - Panic safety: a panic in a spawned tokio task no longer restores the terminal under a still-running TUI. Panics now surface as an in-app
internal errorflash viaMsg::Panic; main-thread panics keep the ratatui restore. The cursor is re-shown on every exit path. - Char-safe truncation: four hand-rolled truncate helpers consolidated into
text::ellipsize(). The fleet one byte-sliced at index 59 and panicked on multi-byte UTF-8 in connection errors. - Rightsize honesty: failed PromQL queries propagate as
Noneand render as—with a warning banner, instead of becoming0.0inside the recommendation math. - Smaller:
checked_mulin config duration parsing, a type guard beforeserde_json's panickingIndexMuton CronJob annotations, guardedgon an empty xray list. - Performance: frame coalescing (event storm costs at most ~60 renders/s); sort-key caching per
resourceVersion(no re-extracting sort cells or re-gunzipping Helm secrets per event); viewport windowing (no more materializing every filtered row per frame); structured filters extract only the named column per object; log filtering is allocation-free for ASCII patterns; rightsize runs its 8 queries × N containers concurrently over a process-wide hyper client.
Full Changelog: v0.14.1...v0.15.0