# 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](#1-core-concepts) 2. [UI Component Taxonomy](#2-ui-component-taxonomy) 3. [Page Structure](#3-page-structure) 4. [Navigation & Deep Links](#4-navigation--deep-links) 5. [SSE Streaming Events](#5-sse-streaming-events) 6. [GraphQL Conventions](#6-graphql-conventions) 7. [UI Labels & Copy](#7-ui-labels--copy) 8. [Iconography](#8-iconography) 9. [State Terminology](#9-state-terminology) 10. [Conventions](#10-conventions) 11. [Appendix: Hermes API Mapping](#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](#2-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. ### Deep Link 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](#4-navigation--deep-links). --- ## 2. UI Component Taxonomy Every component has this shape: ```json { "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`:** ```json { "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`:** ```json { "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: ```json { "page": { "id": "string", "header": { ... } | null, "items": [ ... ], "footer": { ... } | null } } ``` ### Header ```json { "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 | ### Footer The footer is either `null` (no footer) or contains the compose bar for chat pages. There is no pagination — all lists load in full. ```json // 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`): ```json { "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. --- ## 4. Navigation & Deep Links 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. ### Link Format ``` keryx:///?query=val ``` ### Deep Link Registry | 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. ```json 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. ```json 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). ```json 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). ```json 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. ```json 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: ```json 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: ```graphql 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.*