Melian

Chat

Overview

The chat view (/glade) is Melian's primary interface: a full-height conversation window built on the Nan Elmoth atmosphere, with message bubbles styled distinctly for Melian and the user, collapsible tool call visualization, a streaming thinking indicator, and a manuscript-style compose frame. The interface communicates with the server over a WebSocket for streaming responses.

Layout

The chat view uses a three-column layout on wide viewports:

┌─────────────────────────────────────────────────────┐
│  IconRail  │       Message List        │  (reserved) │
│  (64px)    │    (flex 1, scrollable)   │             │
│            ├───────────────────────────┤             │
│            │       Compose Frame       │             │
└─────────────────────────────────────────────────────┘

On mobile (< 640px), the IconRail collapses to a bottom navigation bar and the message list takes the full width.

IconRail (Sidebar)

A narrow sidebar on the left edge containing three bucket buttons:

  • Threads: opens a slide-over panel listing recent conversation threads, sorted by last-activity. Clicking a thread loads it into the message list.
  • Messages: opens a slide-over panel showing iMessage conversations and status.
  • Watch: opens a slide-over panel showing job run status (same data as JobsCard, but read-only here).

The rail uses --moss-1 as its background with a 1px solid var(--bark-3) right border. Icons are 20×20px SVGs filled with --cream-2, brightening to --cream-0 on hover.

Message Bubbles

Melian's Messages

.bubble-melian {
  font-family: 'Sorts Mill Goudy', Georgia, serif;
  font-style: italic;
  font-size: var(--text-lg);
  color: var(--cream-0);
  border-left: 2px solid var(--rose);
  box-shadow: -4px 0 12px var(--rose-glow), 0 0 24px var(--rose-glow);
  background: linear-gradient(135deg, var(--moss-1) 0%, rgba(29,44,30,0.6) 100%);
  padding: 1rem 1.25rem;
  border-radius: 4px;
  max-width: min(640px, 90vw);
  animation: msg-bloom 1.0s cubic-bezier(.2,.7,.2,1) both;
}

The rose-pulse animation (3s ease-in-out infinite) runs on the border-left and box-shadow properties, creating a slow breathing glow. The pulse is suppressed under prefers-reduced-motion.

Markdown within Melian's messages is rendered via a lightweight renderer (no external dependencies). Bold text uses Sorts Mill Goudy non-italic (weight 700). Code spans use ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace inside a --moss-3 inlined chip.

User Messages

.bubble-user {
  font-family: 'Atkinson Hyperlegible', system-ui, sans-serif;
  font-size: var(--text-base);
  color: var(--cream-1);
  background: var(--moss-2);
  border-radius: 4px;
  padding: 0.75rem 1rem;
  max-width: min(640px, 90vw);
  margin-left: auto;   /* right-align */
  animation: msg-bloom 1.0s cubic-bezier(.2,.7,.2,1) both;
}

User bubbles are right-aligned (margin-left: auto), creating a visual distinction without color-coding that would break in Angband mode. No border or glow. The asymmetry is intentional.

ToolChip

Tool calls emitted by the LLM are rendered as collapsible chips between Melian's messages:

▶ search_memory { query: "doctor appointment" }   [expanded below]
  Result: Found 3 relevant memories…

The chip header shows:

  • A small caret (▶ / ▼) indicating collapsed/expanded state
  • The tool name in monospace at --text-sm
  • A compact JSON representation of the arguments (truncated at 60 chars; full args visible on expand)
  • A colored status dot: yellow (in-progress), green (success), red (error)

Expanding the chip shows:

  • Full argument JSON, syntax-highlighted using CSS classes (no runtime JS parser)
  • Result content: if the result is a string, rendered as plain text; if JSON, rendered as a collapsible tree
  • Execution duration in milliseconds

Multiple tool calls in a single LLM turn are grouped under a single collapsible "Tool calls" disclosure. Individual chips within the group expand independently.

ThinkingIndicator

While waiting for the first token from the LLM, a thinking indicator appears below the last user message. It shows a rose-pulse dot alongside the active tool label (e.g., "searching memory...", "reading email...") or "listening..." when no tool is active. After 3 seconds of waiting, an elapsed-time counter appears next to the label.

Once the first token streams in, the indicator is replaced by a Melian bubble that grows character-by-character using the same streaming WebSocket message.

Under prefers-reduced-motion, the pulse animation is suppressed and the dot is static.

EmptyState

When a thread has no messages, the message list shows the EmptyState component, a centered welcome screen:

  • A decorative fleuron (❧ or ✦ depending on the font) at --text-display size, animated with rose-pulse-glow (4.5s ease-in-out infinite), a slow, wide ambient glow in --rose-glow
  • A bucket-specific message that changes based on which IconRail bucket is active (Threads, Messages, or Watch)
  • A random Tolkien quote displayed beneath the message in Sorts Mill Goudy italic at --text-sm --cream-2

Compose Frame

The textarea wrapper is a div.compose-frame positioned at the bottom of the chat column:

.compose-frame {
  position: relative;
  background: rgba(20, 28, 20, 0.6);    /* --moss-1 at 60% */
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--bark-3);
  border-radius: 6px;
  padding: 0.75rem 1rem;
  margin: 0.75rem;
}

Corner ornaments are applied via ::before (top-left) and ::after (bottom-right) pseudo-elements (12×12px, border-style: solid, border-color: var(--color-rose)). At rest, opacity is 0.6. When the textarea is focused, opacity rises to 1.

The <textarea> inside the frame is transparent (background: none), borderless, and resizes vertically (resize: none; overflow-y: auto; max-height: 200px). It auto-grows with content via a shadow div technique (mirrors content in a hidden div to measure height).

Submit: Enter sends. Shift+Enter inserts a newline. The send button (right of the textarea) is a 20×20px SVG arrow, disabled and dimmed when the textarea is empty.

Keyboard Shortcuts

Key Action
Enter Send message
Shift+Enter New line in compose
(in empty compose) Load previous sent message
Cmd+K Jump to thread search
Cmd+/ Toggle sidebar
Escape Close any open slide-over

Mobile Layout

At viewport widths below 640px:

  • The IconRail is hidden and replaced by a bottom navigation bar with icons for Chat, Dashboard, Devices, and Settings
  • The compose frame docks to the bottom of the viewport with position: sticky; bottom: 0
  • Message bubbles expand to max-width: 95vw
  • The atmosphere layers are thinned (mist and leaf layers disabled) to reduce paint cost on mobile GPUs
  • backdrop-filter falls back to a solid --moss-1 background on browsers that don't support it (primarily older Android WebView)