keryx-bff/DOMAIN.md
2026-07-05 11:10:11 +10:00

21 KiB

Keryx — Domain Language & Conventions

Purpose: Define every term, convention, and naming rule used across Keryx (iOS app + BFF). The iOS app design, code, UI labels, and deep links must match this document exactly. If a term appears in the UI, a GraphQL query, or a deep link, it is defined here.


Table of Contents

  1. Core Concepts
  2. UI Component Taxonomy
  3. Page Structure
  4. Navigation & Deep Links
  5. SSE Streaming Events
  6. GraphQL Conventions
  7. UI Labels & Copy
  8. Iconography
  9. State Terminology
  10. Conventions
  11. Appendix: Hermes API Mapping

1. Core Concepts

Keryx

The iOS/iPadOS app. Named after the keryx (κήρυξ) — the ancient Greek herald who precedes the main messenger (Hermes). The app is the herald that announces what Hermes has to say.

Keryx BFF

The Python FastAPI + Strawberry GraphQL backend that the iOS app communicates with. Owns the app experience — drives content, layout, labels, and navigation. Not a thin proxy.

Hermes Agent

The underlying AI agent framework running on the user's server. Keryx never talks to Hermes directly; all communication goes through the Keryx BFF.

Session

A single conversation with Hermes. Has an ID, a model, a start time, messages, and token usage. Sessions are created and managed by the Hermes API server. Keryx references them by their Hermes session ID.

Conversation

Synonym for Session in user-facing contexts. In code and APIs, use session. In UI labels, prefer "Conversation" or "Chat".

Message

A single turn in a session. Has a role (user or assistant), content, and a timestamp. Messages are never deleted by Keryx.

Component

A single piece of UI rendered inside a page feed. Every component has a type, an id, data, and actions. See UI Component Taxonomy for the full list.

Feed

The ordered array of items within a page. The iOS app renders components in array order, left-to-right, top-to-bottom.

Page

A full-screen view presented by the iOS app. Every page has the structure {header, items[], footer}. The BFF builds pages; the iOS app renders them.

Workspace

A directory on the Hermes server containing project files the user interacts with. Defined by a single root path in the BFF config. Read-only.

A keryx:// URL that the iOS app uses for all navigation and actions. No screen transitions happen without a deep link. See Navigation & Deep Links.


2. UI Component Taxonomy

Every component has this shape:

{
  "type": "component_type_name",
  "id": "unique_identifier",
  "data": { ... },
  "actions": [
    {"type": "tap|swipe_left|swipe_right", "label": "Action", "style": "normal|destructive", "link": "keryx://..."}
  ],
  "streaming": false
}

streaming is true when the component is still receiving data via the SSE stream. The iOS app may show a shimmer/typing indicator for streaming components.

2.1 session_card

A session/conversation in the sessions list.

Field Type Required Description
id String Hermes session ID
title String? Session title. null if untitled.
model_badge String? Model name shown as a badge (e.g. "deepseek-v4-flash")
message_count Int Total messages in the session
timestamp String Relative time string (e.g. "2m ago", "Yesterday") — generated by BFF
preview String? First ~80 chars of the session for preview

Display rules:

  • If title is null, show the preview as the primary text (truncated to 1 line)
  • If title is set, show title as primary, preview as secondary
  • model_badge shown as a rounded pill
  • timestamp right-aligned
  • Swipe left reveals "Delete" (destructive)

2.2 message_bubble

A single message in a conversation.

Field Type Required Description
id String Message ID from Hermes
role Enum "user" or "assistant"
text String? Message content. May be empty during streaming.
streaming Bool true while assistant is still generating
timestamp Float? Unix timestamp
model String? Model used for this response (assistant only)
attachments [Attachment]? Files attached to this message

Attachment:

{
  "name": "report.pdf",
  "size": 245760,
  "mime_type": "application/pdf"
}

Display rules:

  • User messages: right-aligned, tinted background
  • Assistant messages: left-aligned, default background
  • model shown as a small label above assistant messages
  • Streaming messages show a blinking cursor at end of text

2.3 thinking_block

Collapsible reasoning from the assistant.

Field Type Required Description
id String Unique ID
text String Reasoning content
collapsed_by_default Bool Default: true

Display rules:

  • Tappable header "Thinking..."
  • Expand/collapse animation
  • Grey/dim text when collapsed
  • Header shows token count if available

2.4 tool_call_card

A tool invocation by Hermes (terminal, web search, file read, etc.).

Field Type Required Description
id String Unique ID
tool String Tool name (e.g. "terminal", "web_search")
command String? The command/query
status Enum "running", "completed", "failed"
output String? Tool output (truncated)
duration_ms Int? Execution time

Display rules:

  • Show tool icon + name as a compact card
  • Show command as monospaced text
  • Status indicator: spinner (running), checkmark (completed), X (failed)
  • Tappable to expand full output (post-MVP)

2.5 file_row

A file or directory in the workspace browser.

Field Type Required Description
id String File path (used as identifier)
name String File/directory name
is_directory Bool True if directory
size Int? File size in bytes (null for directories)
mime_type String? MIME type for files
modified_at String? Relative time string
children [FileRow]? When depth > 0, nested files

Display rules:

  • Directory icon (folder) or file icon based on type
  • Tap on directory → navigates into it
  • Tap on file → opens preview (post-MVP: in-app viewer)
  • Indentation for nested children

2.6 code_block

A rendered code snippet inside a message_bubble. When Hermes returns markdown with code fences, the BFF extracts them into standalone components.

Field Type Required Description
id String Unique ID
language String? Language identifier (e.g. "python", "json")
code String The code content
streaming Bool true while still receiving code

Display rules:

  • Monospaced font, syntax highlighting (SF Mono)
  • Language badge in top-right
  • Copy button in top-right (copy to clipboard)
  • Scrollable horizontal for long lines

2.7 divider

A visual separator.

Field Type Required Description
id String Unique ID
label String? Optional text in the divider

Display rules:

  • Thin horizontal line
  • If label present, text centered in the line

2.8 compose_bar

The chat input area. Only appears in the footer of a chat page, never in items.

Field Type Required Description
id String Always "compose"
placeholder String Placeholder text (e.g. "Ask something...")
actions ComposeActions Deep links for send, attach, voice
disabled Bool true while a message is being sent/streaming

ComposeActions:

{
  "send": "keryx://chat/{id}/send",
  "attach": "keryx://chat/{id}/attach",
  "voice": "keryx://chat/{id}/voice"
}

Display rules:

  • Fixed at bottom of screen
  • Text field + send button + attach button + voice button
  • Disabled state: greyed out, non-interactive
  • Shows attachment chip when a file is staged

3. Page Structure

Every page response has this structure:

{
  "page": {
    "id": "string",
    "header": { ... } | null,
    "items": [ ... ],
    "footer": { ... } | null
  }
}

Header

{
  "title": "Conversations",
  "subtitle": "82 total",
  "actions": [
    {"icon": "plus", "label": "New Chat", "link": "keryx://new-chat"}
  ]
}
Field Type Required Description
title String? Main header text
subtitle String? Smaller text below title
actions [LinkAction]? Right-side action buttons

The footer is either null (no footer) or contains the compose bar for chat pages. There is no pagination — all lists load in full.

// Chat page footer
{
  "type": "compose_bar",
  "placeholder": "Ask something...",
  "actions": {
    "send": "keryx://chat/{id}/send",
    "attach": "keryx://chat/{id}/attach",
    "voice": "keryx://chat/{id}/voice"
  },
  "disabled": false
}
Type Description
compose_bar Chat input area (chat page only)
null No footer (settings page, workspace, sessions list)

Empty State

When items is empty, the BFF may include an empty_state field at the page level (not inside header):

{
  "page": {
    "id": "sessions",
    "header": {"title": "Conversations"},
    "empty_state": {
      "icon": "message",
      "title": "No conversations yet",
      "subtitle": "Start a new chat to begin",
      "action": {"label": "Start Chat", "link": "keryx://new-chat"}
    },
    "items": [],
    "footer": null
  }
}

The iOS app shows the empty state view when items.length === 0 and empty_state is present.


All navigation is driven by keryx:// deep links. The iOS app registers the keryx URL scheme and routes links to screens. No screen transitions are hardcoded.

keryx://<action>/<param>?query=val
Link Action Description
keryx://sessions Present Sessions list page
keryx://new-chat Present Create new session → navigate to chat
keryx://chat/{session_id} Present Open chat view for an existing session
keryx://chat/{session_id}/send Action Send message (POST) — triggered by compose bar
keryx://chat/{session_id}/attach Action Attach file (POST) — triggered by compose bar
keryx://chat/{session_id}/voice Action Start voice input — triggered by compose bar
keryx://delete/{session_id} Action Delete a session
keryx://rename/{session_id} Action Rename a session
keryx://workspace Present Workspace file browser page
keryx://workspace/{path} Present Navigate into a directory in workspace
keryx://workspace/read/{path} Present Preview a workspace file
keryx://settings Present Settings page
keryx://release-notes Present Release notes / changelog
keryx://{anything} Fallback Open in system browser or show error

Navigation Rules

  1. Present links push a new page onto the navigation stack (iPhone) or select a column (iPad)
  2. Action links trigger a side effect (POST, DELETE) and are handled without navigation
  3. The app never constructs URLs dynamically — all links come from BFF responses
  4. Unknown link schemes are silently dropped (app doesn't crash)

5. SSE Streaming Events

The SSE protocol for POST /v1/ios/chat/{session_id}/stream.

Event: component

Sent when a new UI component should be appended to the feed.

event: component
data: {"type": "message_bubble", "id": "msg_1", "data": {"role": "user", "text": "Hello"}}

iOS action: Append a new FeedComponent to items[]. Set streaming: true if the component may receive further updates.

Event: chunk

Sent when text content should be appended to an existing component.

event: chunk
data: {"component_id": "msg_2", "text": "def fibonacci"}

iOS action: Find component by component_id in items[]. Append text to its data.text field. Re-render in place.

Event: component_patch

Sent when a component's metadata should be updated (e.g. tool status change).

event: component_patch
data: {"id": "tc_1", "data": {"status": "completed", "output": "src/\ntests/"}}

iOS action: Find component by id. Deep-merge data into the component's existing data. Fields not in data are preserved.

Event: component_complete

Sent when a component is finalised (no more updates).

event: component_complete
data: {"id": "msg_2", "usage": {"input_tokens": 16267, "output_tokens": 20}}

iOS action: Find component by id. Set streaming: false. Merge any additional data fields (e.g. usage).

Event: done

Sent when the stream is complete.

event: done
data: {}

iOS action: Close SSE connection. Re-enable the send button. The compose_bar in the footer should set disabled: false.

Error Handling

If the BFF encounters an error mid-stream:

event: error
data: {"code": "hermes_unreachable", "message": "Hermes API server is not responding"}

iOS action: Show inline error banner at the bottom of the feed. The stream is not closed — the user can retry.


6. GraphQL Conventions

Naming

Convention Rule Example
Queries camelCase appData, sessionList
Mutations camelCase, verb-first sendMessage, deleteSession
Types PascalCase AppData, SessionCard
Fields camelCase modelBadge, messageCount
Enums PascalCase MessageRole, ToolStatus
Enum values SCREAMING_SNAKE_CASE USER, ASSISTANT, COMPLETED

Union Types for Feed Components

The FeedComponent union type uses Strawberry's @strawberry.type with @strawberry.union. The iOS app receives the __typename field to determine which fragment to use:

query SessionList {
  page(id: "sessions") {
    items {
      __typename
      ... on SessionCard {
        id title modelBadge messageCount timestamp preview
      }
      ... on Divider {
        id label
      }
    }
  }
}

Pagination

No pagination. All lists load in full — sessions, workspace trees, and chat message histories. The page query returns every item at once.


7. UI Labels & Copy

Standard Labels

Context Label Notes
App display name Keryx Not "Keryx iOS" or "Keryx Client"
Tab: sessions Chats User-friendly term
Tab: workspace Files
Tab: settings Settings
New chat button New Chat
Delete action Delete Destructive
Rename action Rename
Send button (None — icon only) Paper plane icon
Attach button (None — icon only) Paperclip icon
Voice button (None — icon only) Microphone icon
Empty sessions No conversations yet
Empty workspace No files yet
Model selector (None — label varies) e.g. "DeepSeek V4 Flash"
Streaming indicator (None — visual only) Blinking cursor / shimmer
Connection: loading Connecting...
Connection: error Could not connect With retry button
Session: untitled (Show first 80 chars of preview) Not "Untitled"
Delete confirmation Delete conversation? Action sheet

Tone & Voice

  • Friendly but direct — the app is a tool, not a companion
  • Minimal labels — every visible word serves a purpose
  • No "wizard" or "magic" language — Hermes is an agent, not a magician
  • Use "Chat", not "Chat with Hermes" or "Conversation with AI"
  • Use "Files", not "Workspace Files" or "Project Files"

8. Iconography

SF Symbols Mappings

Context SF Symbol Name Notes
Tab: Chats message.fill
Tab: Files folder.fill
Tab: Settings gearshape.fill
New chat plus
Send paperplane.fill
Attach paperclip
Voice mic.fill
Delete trash.fill
Rename pencil
Copy code doc.on.doc
Expand chevron.down
Collapse chevron.right
Directory folder Non-filled for file rows
File (generic) doc
File (code) doc.text
File (image) photo
File (PDF) doc.richtext
Empty state tray
Success checkmark.circle.fill Green
Error xmark.circle.fill Red
Pending ellipsis Animated

9. State Terminology

Connection States

State BFF app_data field iOS App Behaviour
Connected server.hermes_connected: true Normal operation
Hermes Unreachable server.hermes_connected: false Show banner "Hermes is not responding. Responses may be delayed."
BFF Error 500 / timeout Show error screen with retry
Version Mismatch app.force_update_url not null Show force-update screen until app is updated

Session States

State BFF Signal iOS App Behaviour
Loading N/A (page fetch in progress) Shimmer / skeleton
Loaded Items array populated Render feed
Empty empty_state present, items empty Render empty state
Error Error in response Show error with retry button

Component States

State streaming field iOS App Behaviour
Static false Render normally
Streaming true Show shimmer/blinking cursor
Updating true (with chunk events arriving) Append text in place
Complete false (after component_complete) Remove shimmer, show final content

10. Conventions

Naming

Domain Convention Example
Files snake_case session_card, keryx_bff
GraphQL fields camelCase messageCount, createdAt
Component types snake_case message_bubble, tool_call_card
Deep links keryx://resource/action keryx://chat/{id}/send
SSE events lowercase_snake component_complete, component_patch
Config keys snake_case staging_cleanup_days
HTTP headers Pascal-Case Authorization, Content-Type
Enum values (Swift) lowerCamelCase .user, .assistant
Enum values (Python) SCREAMING_SNAKE USER, ASSISTANT

Dates & Times

  • BFF → iOS: Relative time strings for display ("2m ago", "Yesterday", "3 weeks ago")
  • BFF → iOS: Unix timestamps (Float) for sorting
  • BFF generation: Use user's timezone (configured in BFF, or sent by iOS app in headers)
  • iOS display: Never show raw timestamps to users

Files

  • File sizes returned as bytes (Int). iOS converts for display (KB, MB, etc.)
  • MIME types follow standard IANA values (application/pdf, text/plain, image/png)
  • Workspace paths are always relative to the configured workspace root. The BFF resolves absolute paths internally.

Errors

Error Code HTTP Status Meaning
auth_invalid 401 API key missing or wrong
session_not_found 404 Session ID doesn't exist
file_too_large 413 Upload exceeds max_upload_size_mb
unsupported_file_type 415 MIME type not allowed
hermes_unreachable 502 Hermes API server not responding
hermes_error 502 Hermes returned an error
workspace_out_of_bounds 403 Path traversal attempt blocked
rate_limited 429 Too many requests

11. Appendix: Hermes API Mapping

This documents how BFF concepts map to Hermes API Server endpoints and data.

Keryx Concept Hermes API Endpoint Notes
Session ID API Server session ID Direct 1:1 mapping
Create session POST /api/sessions Returns Hermes session ID
Session list GET /api/sessions BFF transforms to session_card components
Session messages GET /api/sessions/{id}/messages BFF transforms to message_bubble + thinking_block + tool_call_card
Delete session DELETE /api/sessions/{id}
Rename session PATCH /api/sessions/{id} Update title field
Send message POST /api/sessions/{id}/chat/stream SSE stream of events
Model list GET /v1/models Proxied through BFF
Health check GET /v1/health
File upload (Not in Hermes API) BFF handles staging
Workspace files (Not in Hermes API) BFF reads filesystem

This document is the source of truth for all naming, terminology, and conventions used in the Keryx project. If a UI label, GraphQL field, deep link, or code identifier contradicts this document, this document wins.