Kin / How Kin works

The Graphite+ identity

The design language behind the kin interface: a terminal-owned ground with cool Graphite+ structure and colour held in reserve for signal. This page explains the two-channel colour split and the palette behind it.

Read as Markdown

The design language behind the kin interface: a terminal-owned ground with cool Graphite+ structure and colour held in reserve for signal. This page explains the two-channel colour split and the palette behind it.

Cyan voice, periwinkle thinking

One idea organizes the whole interface: kin’s live voice is cyan, its thinking is periwinkle.

Streamed output and the assistant speak in a clean cyan — the live signal. The reasoning trace is a distinct, recessive periwinkle — the private thinking channel. They are two cool hues, deliberately close in value but clearly apart from each other, so you can tell at a glance whether you are reading kin’s answer or watching it reason. The split is the point: a single colour for “kin is talking to you” and a quieter colour for “kin is working it out.”

Those two roles carry through kin’s own chrome — but they stop at the code band. Code in the transcript is content kin is showing you, not kin speaking, so fenced blocks are real syntax highlighting (a Pygments lexer per language) with the conventional editor cues readers already parse: muted-green strings, bone functions, periwinkle types. The signal hues are held back — cyan appears only on keywords, amber only on numbers — so code reads like an editor, not like the brand.

The markdown body

Everything kin says renders through one markdown identity (the KinMarkdown widget), pinned to the role palette identically on both themes rather than inherited from the toolkit’s defaults. Headings descend a quiet monotone ramp — #/## in the cyan voice (h1 additionally underlined, left-aligned like everything else in the transcript), ### in bone, deeper levels in receding greys — all bold, so hierarchy reads by colour temperature instead of style zigzags. Blockquotes sit flat on the ground behind a dim steel rail (the same rail idiom tools and reasoning use — no lifted band). Tables rule themselves with the deep-graphite RULE line and keep their headers off the chroma. List bullets cycle a small quiet set ( ) from the glyph vocabulary and stay grey — structure, not signal. Inline code and fenced code share the same slightly-lifted code band; inline code stays in body bone, because the band alone says “code” — a backticked git push never borrows the amber that elsewhere means attention.

The palette

Colours are named by role, not by hue, so a re-tint is a one-place edit. The role names below are the constants defined in theme.py.

Role Hue Hex Used for
GROUND graphite #101114 Opaque fallback, code bands, and lifted structure
FG bone #E3E3E6 Body text
PRIMARY cyan #5FBFD6 The live signal: kin speaking, the spinner, accents, the wordmark
REASON periwinkle #9E97D2 The private reasoning / thinking channel
SUCCESS green #5FC198 A tool succeeded
WARNING amber #D6A855 Warnings — distinct enough from cyan that the two never read alike
ERROR coral #E5786E Errors and failures
CODE_STRING muted green #83B692 String literals inside the code band — the one content-only hue, never chrome
RULE deep graphite #2A2E36 Rules and the low-contrast scrollbar thumb

These constants are the single place the palette is named. The Textual theme wires them into the UI’s CSS tokens, and the transcript widgets import the same constants directly for Rich text styles (Rich cannot read a CSS variable) — so a styled CSS rule and a hand-built renderable always land on the same colour.

Terminal ground

The default kin theme does not paint the main canvas. It inherits your terminal’s own background, including translucency, while keeping Graphite+ truecolour accents and intentional opaque surfaces such as code bands, diff washes, menus, and modal cards. This is a hybrid theme, not a retreat to the terminal’s ANSI palette: cyan still means live voice, periwinkle still means thinking, status colours keep their roles, and the composer cursor remains an opaque cyan block so the insertion point stays clear on the inherited ground.

The current contract is dark terminals. If your terminal palette is light, or you need a deterministic opaque canvas, run /theme kin-graphite. /theme cycles between the transparent kin theme and that opaque fallback; the choice is saved for future sessions. Any registered Textual theme can still be chosen by name as an explicit escape hatch. Cursor and search-result tints follow that selected theme’s primary colour; only transparent kin uses fixed Graphite+ preblends, because Textual cannot alpha-blend those cues against a terminal-owned background.

Measured palette contrast

The recessive greys and the shimmer base are tuned for WCAG against the opaque GROUND reference (#101114), not chosen by eye — the ratios are measured with the standard relative-luminance formula and live in theme.py next to each constant. Body-text grey (GREY_LO, reasoning tails / diff metadata / pygments comments) clears AA body at 4.65:1; the shimmer base (PRIMARY_DIM, the wordmark hairline + cosine-sweep rest colour) clears AA Large at 3.21:1 while keeping a ~5.7-point delta below PRIMARY so the sweep stays visible. A terminal-owned background can change those measured ratios; use the kin-graphite fallback when guaranteed palette contrast matters.

Glyph vocabulary

Alongside the palette, theme.py owns the functional glyph vocabulary — the full set of disclosure, connector, and chrome glyphs (G_RUNNING, G_CARET_*, G_ELBOW, G_BULLET, G_OK, G_ERR, G_AGENT, G_RETRY, G_PAUSE, G_TODO_*, METER_*, SPINNER_STYLES, …). These are functional — a status, a connector, a spinner frame — not skin, so they’re shared semantics with kin’s Go TUI and they live ONLY in theme.py. A literal glyph anywhere else in kin.tui is an audit-fail: a future re-tint of G_BULLET would otherwise leave stranded literals off-identity. Widgets and renderers import these constants; the same invariant covers palette colours and glyph vocabulary.

Spinner vibes

The busy spinner is choosable by vibe — a small curated set of named styles in theme.py’s SPINNER_STYLES, each a SpinnerStyle (constant cell-width frames + cadence + a one-word descriptor; widths may differ between styles — rain is two cells). Pick one interactively with /spinner or set the spinner key / KIN_SPINNER env directly; resolve_spinner falls back to the default on unknown names and forces the ASCII-safe line style under a truthy KIN_ASCII.

name vibe frames cadence
rain (default) matrix ⠁⡄ ⠋⡄ ⠏⡀ ⠟⡈ … — drips falling down two braille cells, each drop randomly skipping beats and varying its tail (120 deterministic pre-baked frames, seamless ~9.6s loop) 0.08s
braille classic ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ 0.10s
arc orbital ◜ ◠ ◝ ◞ ◡ ◟ 0.12s
toggle metronome ⊶ ⊷ 0.35s
line plain `- \ /`

Exactly one frame-stepped spinner position exists in the chrome — the status bar’s busy glyph — plus its one transcript mirror, a running workflow card; every other live surface expresses liveness by colour only (the shimmer and pulse). That restraint is the “one heartbeat, colour elsewhere” invariant (POLISH r11) in REFERENCE.md.

Why calm

The restraint is deliberate. kin reads as a precision tool, not a dashboard: a quiet terminal field where colour means something every time it appears. The screen, transcript, and collapsibles share one inherited ground, while opaque Graphite+ surfaces are reserved for content that benefits from separation. Nothing competes for attention until it carries signal. That is why the live voice and the thinking channel get the only two saturated cool hues, and why status colours (green, amber, coral) stay reserved for the moments they actually report. Colour is information here, so it is spent sparingly.

Scrollbars follow the same restraint: they use a one-cell transparent trough and a translucent deep-graphite thumb, gaining a little presence on hover and rising to dim cyan only while actively dragged. The resting thumb stays close to the ground, leaving scrollback locatable without adding a bright rail to the edge of the conversation. The transcript keeps its left reading inset while the scrollbar occupies the far-right cell.