# Compliance Review -- discordo-plus Reviewed: 2026-03-20 | Reviewer: Claude (automated) Scope: all source files | Standard: General (Code Quality, Performance, Go Best Practices) Reference: `research/TECHFILE.md` for dependency and version context. --- ## CRITICAL ### 1. ✅ FIXED — Cache.Get Panics on Missing Key `internal/cache/cache.go:27` -- The `Get` method performs an unchecked type assertion `i.(uint)` on the result of `sync.Map.Load`. If the key does not exist, `i` is `nil` and the assertion panics. While `Exists` is called before `Get` in the current codebase (`message_input.go:499-500`), there is no guarantee callers will always check first, and the race window between `Exists` and `Get` on a `sync.Map` means concurrent `Invalidate` could delete the key between the two calls, causing a runtime panic. **Fix:** Change `Get` to return `(uint, bool)` or use a comma-ok assertion: `if i, ok := c.items.Load(query); ok { return i.(uint) }; return 0`. Update callers to handle the zero/missing case. **Implementation Risk:** Low. Only `message_input.go` calls `Get`, and the change is a straightforward API update. The fix also closes a potential TOCTOU race. --- ### 2. ✅ FIXED — HTTP Response Body Leak in Brotli Transport `internal/http/transport.go:28-32` -- When the response has `Content-Encoding: br`, the original `resp.Body` is wrapped with `brotli.NewReader` via `io.NopCloser`. This means calling `Close()` on the new body is a no-op and the original body's connection is never returned to the pool. For long-running applications like a Discord client, this can exhaust available connections over time. **Fix:** Replace `io.NopCloser(brotli.NewReader(resp.Body))` with a custom `ReadCloser` that wraps the brotli reader but delegates `Close()` to the original `resp.Body`. Example: `type brotliReadCloser struct { io.Reader; closer io.Closer }` with `func (b *brotliReadCloser) Close() error { return b.closer.Close() }`. **Implementation Risk:** Low. The fix is a small wrapper struct. Verify with a test that the underlying body is closed when the brotli-wrapped response body is closed. --- ### 3. ✅ FIXED — Notification Avatar Download Uses Bare http.Get Without Timeout `internal/notifications/notifications.go:93` -- `http.Get(url)` uses the default HTTP client which has no timeout. A slow or unresponsive Discord CDN server will block the goroutine indefinitely. Since notifications are triggered from gateway events, this could accumulate blocked goroutines during network issues. **Fix:** Use `http.Client{Timeout: 10 * time.Second}` or create a request with `context.WithTimeout` and pass it to `http.DefaultClient.Do(req)`. Apply the same fix to `downloadToCache` in `messages_list.go:1191` which also uses bare `http.Get`. **Implementation Risk:** Low. Adding a timeout may cause some avatar downloads to fail under poor network conditions, but this is preferable to indefinite blocking. Existing error handling already covers the failure path. --- ### 4. ✅ FIXED — Log File Opened With os.ModePerm (0777) `internal/logger/logger.go:24` -- The log file is opened with `os.ModePerm` (0777), meaning it is world-readable and world-writable. Log files may contain sensitive debugging information (user IDs, channel names, error messages). Similarly, `internal/consts/consts.go:25` creates the cache directory with `os.ModePerm`. **Fix:** Use `0600` for the log file (owner read/write only) and `0700` for the cache directory (owner only). Apply the same change to `guildstate.go:45` which writes state with `0644` (already better but could be `0600` for consistency). **Implementation Risk:** Low. Users running in shared environments benefit from restrictive permissions. Existing users may need to manually fix permissions on existing files if they switch. --- ## CODE QUALITY ### 5. ✅ FIXED — messages_list.go Is Over 1600 Lines -- God File `internal/ui/chat/messages_list.go` -- Extracted into focused files: (a) `embed_renderer.go` for embed types, styles, line wrapping, URL helpers, and markdown unescape; (b) `attachment_handler.go` for download, view, save, and overlay methods; (c) `url_extractor.go` for URL extraction from message content and embeds. The main file now focuses on message rendering and keybinds. --- ### 6. ✅ FIXED — Duplicated ShortHelp/FullHelp Logic in guilds_tree.go `internal/ui/chat/guilds_tree.go:67-132` -- The `ShortHelp()` and `FullHelp()` methods contain nearly identical logic for determining the `selectDesc` string based on node state (lines 69-86 duplicated at 100-117). This 18-line block is copy-pasted. **Fix:** Extract a helper method `func (gt *guildsTree) selectActionDesc() string` that returns "collapse", "expand", or the default description based on node state. Call it from both `ShortHelp` and `FullHelp`. **Implementation Risk:** Low. Pure refactoring of display logic with no behavioral change. --- ### 7. ✅ FIXED — Duplicated attachmentsPicker Layer Setup `internal/ui/chat/messages_list.go:1178-1187` and `internal/ui/chat/messages_list.go:1318-1327` -- The code to show the attachments picker overlay is identical in `showAttachmentsList` and `saveImage`: create centered layer, add with overlay options, send to front, set focus. This 10-line pattern is duplicated verbatim. **Fix:** Extract a method `func (ml *messagesList) showAttachmentsOverlay()` that handles the layer setup and focus. Call it from both `showAttachmentsList` and `saveImage` after setting items. **Implementation Risk:** Low. Simple extraction of identical code. --- ### 8. ✅ FIXED — Inconsistent Error Handling: Some Paths Silently Ignore Errors `internal/ui/chat/messages_list.go:1409` -- `me, _ := ml.chatView.state.Cabinet.Me()` discards the error. If the state is not ready, `me` is nil and `me.ID` will panic on the next line. This pattern appears in multiple places: `state.go:195`, `messages_list.go:1446`, `messages_list.go:1539`, `messages_list.go:1563`. **Fix:** Handle the error case: `me, err := ml.chatView.state.Cabinet.Me(); if err != nil { slog.Error(...); return }`. Apply to all instances where `Me()` is called with a discarded error. **Implementation Risk:** Low. Adding error checks prevents nil-pointer panics. The `Me()` call should always succeed after login, but defensive coding prevents crashes during edge-case race conditions (e.g., state reconnection). --- ### 9. ✅ FIXED — Magic Number: embedLineStyles Uses Hardcoded Array Size [8] `internal/ui/chat/messages_list.go:670` -- `[8]tcell.Style{}` is a magic number that must manually stay in sync with the number of `embedLineKind` constants (lines 660-668). Adding a new embed line kind without updating the array size causes a silent bug (zero-value style). **Fix:** Define a sentinel `const embedLineKindCount = 8` after the last `iota` constant, or use a slice. Example: add `embedLineKindCount` as the last iota entry and use `[embedLineKindCount]tcell.Style{}`. **Implementation Risk:** Low. Compile-time enforcement prevents future mismatches. --- ### 10. ✅ FIXED — guildState Uses Mutex Where RWMutex Would Be Appropriate `internal/ui/chat/guildstate.go:14-17` -- `guildState` uses `sync.Mutex` for all access, but `isExpanded` (line 61) is a read-only operation that could use `RLock`. Guild state is read frequently (every node creation during `onReady`) and written infrequently (user expand/collapse action). **Fix:** Change `mu sync.Mutex` to `mu sync.RWMutex` and use `gs.mu.RLock()`/`gs.mu.RUnlock()` in `isExpanded`. **Implementation Risk:** Low. Standard Go concurrency pattern upgrade. --- ### 11. ✅ FIXED — Inconsistent Log Level Usage Multiple files -- Fixed: keyring retrieval failure (`root/events.go`) and avatar cache failure (`notifications.go`) now log at `slog.Warn` instead of `slog.Info`. Presence lookup (`message_input.go`) remains at `slog.Info` as it is a normal condition. Added nil guard for `Me()` in `state.go:onTypingStart` to prevent potential panic. --- ### 12. ✅ FIXED — Invalid Log Level Silently Defaults to Zero Value `cmd/root.go:29-39` -- The `switch` on `logLevel` has no `default` case. An invalid value like `--log-level=trace` silently results in `slog.Level(0)` which is `slog.LevelInfo`. The user gets no feedback that their log level was invalid. **Fix:** Add a `default` case that either returns an error (`return fmt.Errorf("unknown log level: %q", logLevel)`) or logs a warning and falls back to info. **Implementation Risk:** Low. Adds user feedback for misconfiguration. ### 25. ✅ FIXED — Discarded Me() Errors in Reaction Handlers `internal/ui/chat/state.go` -- Reaction event handlers (`onMessageReactionAdd`, `onMessageReactionRemove`) called `Me()` and discarded the error, risking nil-pointer panics if the cabinet state was not ready. **Fix:** Added nil guards and error documentation for all `Me()` call sites in reaction handlers. --- ### 26. ✅ FIXED — Discarded Me() Errors in message_input.go `internal/ui/chat/message_input.go` -- Additional `Me()` calls with discarded errors in message composition paths, separate from finding #8. **Fix:** Added nil guards with early returns to prevent nil-pointer panics. --- ### 27. ✅ FIXED — emojiFavorites.save() Holds RLock During Write `internal/ui/chat/emoji_picker.go` -- The `save()` method internally acquired a read lock (`RLock`) but performed a write operation (JSON marshal + file write), creating a lock-type mismatch. Callers holding the write lock would deadlock. **Fix:** Refactored to caller-must-hold-lock pattern. `save()` no longer acquires its own lock; callers hold the write lock through the entire read-modify-save cycle. --- ### 28. ✅ FIXED — guildState.save() Double-Lock `internal/ui/chat/guildstate.go` -- The `save()` method acquired the mutex internally, but some callers already held the lock, creating a potential double-lock deadlock with `sync.Mutex` (non-reentrant in Go). **Fix:** Removed the internal lock acquisition from `save()`. All callers now hold the lock before calling save. --- ### 29. ✅ FIXED — Inconsistent Picker Close/Browse Reset `internal/ui/chat/emoji_picker.go`, `search_picker.go`, `channels_picker.go` -- Different picker types handled close and browse-mode reset inconsistently. Some reset browse state on close, others did not. **Fix:** Added comments explaining the intentional differences between picker close behaviors. Each picker's reset logic is appropriate for its use case (e.g., emoji picker preserves category state). --- ### 30. ✅ FIXED — attachmentsPicker Shared Helpers `internal/ui/chat/attachments_picker.go` -- The attachments picker duplicated help text formatting logic that could use shared picker helpers. **Fix:** Updated to use `pickerShortHelp` and `pickerFullHelp` shared helpers from `util.go`. --- ### 31. ✅ FIXED — Repeated Me() Calls in Help `internal/ui/chat/messages_list.go` -- Help text generation called `Me()` on every render frame to determine user-specific help entries. **Fix:** Cached help data to avoid repeated `Me()` calls during help rendering. --- ### 32. ✅ FIXED — atomicSaveJSON Temp File Cleanup `internal/ui/chat/guildstate.go`, `emoji_picker.go` -- The atomic save pattern (write temp + rename) did not clean up the temp file on rename failure, leaving orphaned `.tmp` files. **Fix:** Added `os.Remove(tmp)` on rename failure to clean up temp files. --- ### 33. ✅ FIXED — New Files Missing Documentation `internal/ui/chat/emoji_picker.go`, `search_picker.go`, `user_info.go`, `command_input.go` -- Newly added files lacked package-level and function-level doc comments. **Fix:** Added doc comments to all exported and significant unexported functions in new files. --- ### 34. ✅ FIXED — pickerBrowseHandleKey Undocumented `internal/ui/chat/util.go` -- The `pickerBrowseHandleKey` function handled browse-mode keybinds (j/k/g/G/i) for overlay pickers but had no doc comment explaining the mode or keybind semantics. **Fix:** Expanded doc comment explaining browse mode, supported keys, and how it integrates with picker focus. --- ### 35. ✅ FIXED — Reaction Event Handlers Mutate In-Place `internal/ui/chat/state.go` -- Reaction add/remove handlers mutated the cached message's `Reactions` slice in place rather than creating a copy. While safe in the current single-threaded event loop, this pattern is fragile. **Fix:** Documented the in-place mutation pattern with comments explaining why it is safe (all mutations occur on the UI goroutine via `HandleEvent`). --- ## DOCUMENTATION GAPS ### 13. ✅ FIXED — No File-Level Comments on Most Source Files Added package-level doc comments to `internal/ui/chat/` (package comment in `model.go`) and `internal/markdown/` (package comment in `renderer.go`). File-level doc comment added to `events.go` explaining the event/command architecture. --- ### 14. ✅ FIXED — No Documentation on the Event/Command Architecture Added doc comment to `internal/ui/chat/events.go` explaining the command/event pattern: commands run off the main goroutine and return events dispatched to HandleEvent on the UI thread. Package-level doc in `model.go` describes the overall architecture. --- ### 15. ✅ FIXED — No Documentation on the Rendering Pipeline Added package-level doc comment to `internal/markdown/renderer.go` explaining the AST→styled lines pipeline: goldmark AST walking, tcell style application for inline/block elements, and Discord-specific nodes (mentions, emoji, OSC 8 URLs). --- ### 16. ✅ FIXED — Config Validation Rules Undocumented Added validation comments to `AutocompleteLimit` and `MessagesLimit` fields in `config.go`. Added `DISCORDO_TOKEN` security warning to `config.toml`. Improved `image_viewer_args` documentation in `config.toml`. --- ## PERFORMANCE ### 17. Unbounded itemByID Cache in messagesList `internal/ui/chat/messages_list.go:58` -- `itemByID map[discord.MessageID]*tview.TextView` caches rendered message TextViews and is only cleared on channel switch (`reset()`) or when messages are modified. For channels with heavy traffic, this map grows without bound as new messages arrive. Each `tview.TextView` holds rendered line data, styles, and a text buffer. Over a long session in a busy channel, this can accumulate thousands of entries. **Fix:** Add a size cap. When `len(itemByID) > maxCacheSize` (e.g., 500), evict entries for message IDs no longer in the current `messages` slice. Alternatively, clear the cache when it exceeds a threshold during `addMessage`. **Implementation Risk:** Low-medium. Cache eviction during message additions may cause brief re-rendering of older messages if the user scrolls up. The visual impact is negligible since re-rendering is the fallback path. --- ### 18. ✅ FIXED — collapseParentNode Uses O(n) Tree Walk `internal/ui/chat/guilds_tree.go:380-395` -- `collapseParentNode` walks the entire tree from root to find the parent of the given node, even though `GetPath(node)` (used in `canCollapseParent`) returns the full path including the parent. The O(n) walk runs on every `-` keypress. **Fix:** Use `gt.GetPath(node)` to get the parent directly: `path := gt.GetPath(node); if len(path) >= 3 { parent := path[len(path)-2]; ... }`. This is O(depth) instead of O(total nodes). **Implementation Risk:** Low. `GetPath` is already used in `canCollapseParent` and `expandPathToNode`, so it is a known-reliable method. Verify the path includes the root node as the first element. --- ### 19. ✅ FIXED — Repeated Full-Tree GetPath Calls in canCollapseParent `internal/ui/chat/guilds_tree.go:134-145` -- `canCollapseParent` is called from both `ShortHelp()` and `FullHelp()`, which run on every draw cycle to update the help bar. Each call invokes `gt.GetPath(node)` which walks the tree. Combined with `ShortHelp` and `FullHelp` duplicating the node-state checks, the help bar triggers 2+ tree walks per render frame. **Fix:** Cache the path result for the current node, or compute `canCollapseParent` once and store it. Since the current node only changes on explicit user navigation, the cached value remains valid between navigation events. **Implementation Risk:** Low. Cache invalidation happens naturally when `SetCurrentNode` is called. --- ### 36. ✅ FIXED — messageURLs Called Twice Per Render `internal/ui/chat/messages_list.go` -- The `messageURLs()` helper was called twice for the same message during rendering: once for the help bar and once for the attachment overlay. **Fix:** Cached the result per cursor position so it is computed once and reused. --- ### 37. ✅ FIXED — searchPicker.update Copies All Messages `internal/ui/chat/search_picker.go` -- The search update function used `fmt.Sprintf` to build search strings for every message on each keystroke, generating unnecessary allocations. **Fix:** Replaced `fmt.Sprintf` with direct string concatenation to reduce allocations. --- ### 20. extractURLs Parses Message Content Twice `internal/ui/chat/messages_list.go:1094-1115` and `internal/ui/chat/messages_list.go:511-519` -- When rendering a message with embeds, `drawEmbeds` calls `extractURLs(message.Content)` to build a dedup set, and then `drawContent` also parses the same content through `discordmd.ParseWithMessage` for markdown rendering. The goldmark parser is invoked twice on the same content. For messages with complex markdown, this is measurable. **Fix:** Parse content once and pass both the rendered lines and the extracted URLs to the embed renderer. This requires restructuring `drawDefaultMessage` to call a combined parse+render function that returns both outputs. **Implementation Risk:** Medium. The rendering pipeline currently separates content rendering from embed rendering. Merging them requires careful handling of the `forceMarkdown` flag used in embed descriptions. --- ## INFRASTRUCTURE ### 21. ✅ FIXED — CI Uses go-version: stable -- Not Pinned `.github/workflows/ci.yml:25` -- As documented in `research/TECHFILE.md` (finding #5), the CI uses `go-version: stable` which auto-advances when Go releases a new version. This can introduce unexpected breakage from new vet rules or behavior changes. **Fix:** Pin to `go-version: "1.26"` or `"1.26.x"`. Update intentionally when upgrading. **Implementation Risk:** None from pinning. The risk is the current unpinned behavior. --- ### 22. ✅ FIXED — No Linter in CI `.github/workflows/ci.yml` -- The CI pipeline only ran `go test` and `go build` with no linter step. **Fix:** Added `golangci-lint` step to CI workflow with default linters (`vet`, `errcheck`, `staticcheck`, `unused`). **Implementation Risk:** None. --- ### 23. ✅ FIXED — CI Artifact Name Does Not Match Binary Name `.github/workflows/ci.yml:41-43` -- CI uploads artifact named `discordo_$OS_$ARCH` but the binary is `discordo` (or `discordo.exe`). The project README and CLAUDE.md refer to the binary as `discordo-plus`. There is a naming inconsistency between the upstream module path (`github.com/ayn2op/discordo`), the CI artifact name, and the intended installation name. **Fix:** Update the build step to `go build -trimpath -ldflags=-s -o discordo-plus .` (or `discordo-plus.exe` on Windows) to match the documented binary name. Update the artifact upload to reference the correct file. **Implementation Risk:** Low. Downstream users of CI artifacts will see the renamed binary. Scoop/AUR packages may need path updates. ### 38. ✅ FIXED — PKGBUILD Missing Build Flags `PKGBUILD` -- The Arch Linux package build script did not include `-trimpath` or `-ldflags=-s`, producing larger binaries with embedded filesystem paths compared to CI builds. **Fix:** Added `-trimpath -ldflags=-s` to the PKGBUILD build command, matching the CI configuration. **Implementation Risk:** None. --- ## TEST COVERAGE ### 24. Only 2 Packages Have Tests -- Minimal Coverage `internal/config/config_test.go`, `internal/keyring/keyring_test.go` -- Out of 12 internal packages, only 2 have any tests. Zero test coverage for: markdown rendering, clipboard operations, HTTP transport, notifications, cache, guild state persistence, UI components, embed processing, URL extraction, and the entire event handling pipeline. Critical untested code paths: - `cache.go:Get` -- can panic (finding #1) - `transport.go` -- body leak (finding #2) - `guildstate.go` -- JSON marshal/unmarshal roundtrip - `messages_list.go` -- `extractURLs`, `messageURLs`, `wrapStyledLine`, `unescapeMarkdownEscapes`, `linkDisplayText`, `sameLocalDate` are all pure functions ideal for unit testing - `config/theme.go` -- TOML unmarshaling for all wrapper types - `util.go` -- `MergeStyle`, `SortGuildChannels`, `ChannelToString` **Fix:** Prioritize tests for: (a) pure functions in messages_list.go (URL extraction, wrapping, escaping), (b) cache.go roundtrip and race conditions, (c) guildstate.go persistence roundtrip, (d) theme.go unmarshaling edge cases. These require no mocking and have clear input/output contracts. **Implementation Risk:** None. Tests are additive. --- ## POSITIVES - Go 1.26 module minimum is the latest stable release; dependency versions are current (see TECHFILE.md). - Error wrapping with `%w` is used consistently in config loading, HTTP transport, and attachment handling. - The event/command architecture cleanly separates I/O from UI updates; all gateway operations run in commands (closures), keeping the UI thread responsive. - Config defaults are embedded via `//go:embed`, eliminating runtime file-not-found failures. - Token storage uses the OS keyring (zalando/go-keyring) rather than plaintext files. - Guild state persistence uses proper mutex synchronization around both reads and writes. - The typing indicator implementation correctly uses `time.AfterFunc` with cleanup, preventing timer leaks. - `selectedChannelMu` RWMutex correctly protects the selected channel across goroutines. - The markdown renderer's style stack pattern is clean and handles nested formatting correctly. - The embed deduplication logic (`embedLineDedupKey`) prevents duplicate content across embed fields. - Platform-specific code uses Go's filename-based build tag convention cleanly (clipboard, editor, suspend). - The `ConfigurePicker` helper in `util.go` avoids duplicating picker setup across three picker types. - `wrapStyledLine` correctly uses grapheme cluster width via `uniseg` for accurate wrapping with wide glyphs and emoji. --- ## Summary | Severity | Total | Fixed | |----------|-------|-------| | Critical | 4 | 4 | | Code Quality | 19 | 19 | | Documentation Gaps | 4 | 4 | | Performance | 6 | 4 | | Infrastructure | 4 | 4 | | Test Coverage | 1 | 0 | --- ## Files Reviewed | # | File | Type | |---|---|---| | 1 | `main.go` | Entry point | | 2 | `cmd/root.go` | CLI / app init | | 3 | `internal/config/config.go` | Config struct + loader | | 4 | `internal/config/config.toml` | Embedded default config | | 5 | `internal/config/config_test.go` | Config tests | | 6 | `internal/config/keybinds.go` | Keybind definitions | | 7 | `internal/config/theme.go` | Theme TOML unmarshaling | | 8 | `internal/config/editor.go` | Editor command (merged from editor_unix.go + editor_default.go) | | 10 | `internal/consts/consts.go` | App name + cache dir | | 11 | `internal/logger/logger.go` | slog file logger | | 12 | `internal/keyring/keyring.go` | Token keyring wrapper | | 13 | `internal/keyring/keyring_test.go` | Keyring tests | | 14 | `internal/cache/cache.go` | Autocomplete cache | | 15 | `internal/http/transport.go` | HTTP transport (gzip + brotli) | | 16 | `internal/http/client.go` | API client constructor | | 17 | `internal/http/headers.go` | Request headers | | 18 | `internal/http/props.go` | Discord identify properties | | 19 | `internal/notifications/notifications.go` | Notification dispatch | | 20 | `internal/notifications/desktop_toast.go` | Non-darwin notification | | 21 | `internal/notifications/desktop_toast_darwin.go` | macOS notification | | 22 | `internal/clipboard/clipboard.go` | Clipboard format types | | 23 | `internal/clipboard/clipboard_default.go` | Default clipboard impl | | 24 | `internal/clipboard/clipboard_wayland.go` | Wayland clipboard impl | | 25 | `internal/markdown/renderer.go` | Discord markdown renderer | | 26 | `internal/ui/util.go` | Shared UI helpers | | 27 | `internal/ui/root/model.go` | Root model + help overlay | | 28 | `internal/ui/root/events.go` | Root events (token, clipboard) | | 29 | `internal/ui/root/keybinds.go` | Root keymap | | 30 | `internal/ui/root/suspend_unix.go` | Unix SIGTSTP suspend | | 31 | `internal/ui/root/suspend_default.go` | No-op suspend | | 32 | `internal/ui/chat/model.go` | Chat model + layout | | 33 | `internal/ui/chat/state.go` | Gateway state commands | | 34 | `internal/ui/chat/events.go` | Chat event handlers | | 35 | `internal/ui/chat/keybinds.go` | Chat keymap | | 36 | `internal/ui/chat/guilds_tree.go` | Guild/channel tree | | 37 | `internal/ui/chat/guildstate.go` | Guild expand persistence | | 38 | `internal/ui/chat/messages_list.go` | Message rendering + keybinds | | 38a | `internal/ui/chat/attachment_handler.go` | Attachment download/view/save | | 38b | `internal/ui/chat/embed_renderer.go` | Embed types + line wrapping | | 38c | `internal/ui/chat/url_extractor.go` | URL extraction from messages | | 39 | `internal/ui/chat/message_input.go` | Message input + mentions | | 40 | `internal/ui/chat/attachments_picker.go` | Attachment picker | | 41 | `internal/ui/chat/channels_picker.go` | Channel picker | | 42 | `internal/ui/chat/mentions_list.go` | Mentions autocomplete list | | 43 | `internal/ui/chat/util.go` | Picker config + humanJoin | | 44 | `internal/ui/login/model.go` | Login model | | 45 | `internal/ui/login/events.go` | Login events | | 46 | `internal/ui/login/keybinds.go` | Login keymap | | 47 | `internal/ui/login/token/model.go` | Token login form | | 48 | `internal/ui/login/token/events.go` | Token events | | 49 | `internal/ui/login/qr/model.go` | QR login model | | 50 | `internal/ui/login/qr/events.go` | QR login events + crypto | | 51 | `go.mod` | Module manifest | | 52 | `.github/workflows/ci.yml` | CI configuration | | 53 | `.gitignore` | Git ignore rules | | 54 | `README.md` | Project readme | | 55 | `CLAUDE.md` | Project context | | 56 | `internal/config/config.toml` | Default config (embedded) |