Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/marchat.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Prefer this repo (`go.mod`, `ARCHITECTURE.md`, `PROTOCOL.md`, skills) as source
- **Themes**: built-in + **custom JSON themes** under client config dir (`theme_loader.go`)
- **Notifications**: bell, desktop (**Alt+N**), **`:notify-mode`**, quiet hours (**`:quiet`**), focus mode (**`:focus`**) - `notification_manager.go` + help text
- **Mentions**: `@user` highlighting in render; mention-aware notification path - see `client/render.go`
- **Transcript wrap**: ANSI-aware word wrap in `wrapStyledBlock` / `renderMessages`; long URLs use non-breaking hyphens and `ansi.Wrap` path breakpoints; hyperlink style applied per wrapped segment via URL span markers (`markURLsForWrap`, `applyURLMarkers`). Mouse click-to-open uses manual coordinate mapping and is **not reliable** for wrapped long URLs; copy from message or see `ROADMAP.md` / GitHub issue.
- **Transcript wrap**: ANSI-aware word wrap in `wrapStyledBlock` / `renderMessages`; long URLs use non-breaking hyphens and `ansi.Wrap` path breakpoints; hyperlink style and OSC 8 sequences applied per wrapped segment via URL span markers (`markURLsForWrap`, `applyURLMarkers`). Mouse click-to-open fallback uses manual coordinate mapping and is **not reliable** for wrapped long URLs; OSC 8 terminals should use the embedded hyperlinks; copy from message otherwise ([#103](https://github.com/Cod-e-Codes/marchat/issues/103)).
- **Client-local System lines**: ephemeral command feedback uses the banner; negative `message_id` transcript notices (themes, search, channel lists) stay in the transcript when `isTranscriptSystemNotice` matches and carry the active channel. `pruneEphemeralSystemMessages` on send or inbound persisted message removes only non-transcript System lines.
- **Profiles**: `profiles.json`; **dedupe** display names on load; default **Profile-N** naming when adding profiles (`client/config/config.go`)
- **Config paths**: app data dir or **`MARCHAT_CONFIG_DIR`** via **`ResolveClientConfigDir()`**; **`GetConfigPath()`** uses the same rules as keystore primary path; **`GetKeystorePath`** checks primary -> user-dir `keystore.dat` when override has none -> last legacy **`./keystore.dat`** in cwd - `MigrateKeystoreToNewLocation`
Expand Down
3 changes: 2 additions & 1 deletion .cursor/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Update domain skills when shipped behavior changes. Recent fixes on `main` (or i

- Reconnect backoff advances on failure (not reset each `Init()`); channel stamping on server outbound messages
- Client transcript notices: negative `message_id` classified by content; scoped to active channel
- URL click: partial headless helpers (`buildTranscriptLineURLs`, `chatPanelOrigin`); wrapped long URLs still fail in manual testing; [#103](https://github.com/Cod-e-Codes/marchat/issues/103); workaround is copy/paste
- URL click: OSC 8 hyperlinks on wrapped segments (Lip Gloss v2); manual click fallback remains unreliable for wrapped long URLs; copy/paste when needed ([#103](https://github.com/Cod-e-Codes/marchat/issues/103))
- Charm v2: `charm.land/*/v2`, `tea.View` + `KeyPressMsg`, overlay scroll/input routing in `scroll_input.go`
- `:backup` SQLite-only; Postgres/MySQL migrations use `BOOLEAN DEFAULT FALSE`

## Skill index
Expand Down
11 changes: 7 additions & 4 deletions .cursor/skills/client-marchat/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ paths:

# Client (marchat)

Bubble Tea + Lipgloss TUI. Entry: `client/main.go`; split across `render.go`, `commands.go`, `hotkeys.go`, `websocket.go`, `cli_output.go`, `notification_manager.go`, etc.
Bubble Tea + Lipgloss TUI on **Charm v2** (`charm.land/bubbletea/v2`, `bubbles/v2`, `lipgloss/v2`). Entry: `client/main.go`; split across `render.go`, `commands.go`, `hotkeys.go`, `websocket.go`, `scroll_input.go`, `cli_output.go`, `notification_manager.go`, etc.

## Patterns

- **Charm v2**: `newMainTeaView` sets `AltScreen`, `MouseModeCellMotion` (disabled while Shift is held for terminal drag-select), and `tea.View.BackgroundColor` (`altScreenFill` / black). `chromeComposerPanel` is full-width with theme `Input` background only (textarea styles are foreground-only). Transcript interior uses `transcriptFill` on `Box`. `configureTextareaChrome` syncs textarea colors with theme `Input`. Multiline composer: Ctrl+J via `textarea.Update`; up/down move cursor when value contains `\n`, else scroll chat. `KeyPressMsg` / `KeyReleaseMsg`; bubbles use `SetWidth` / `SetHeight` / `SetStyles`.
- **Reconnect**: exponential backoff (capped at 30s); delay resets only after successful connect (`wsConnected`), not each `Init()` retry; no reconnect on fatal username/handshake errors (`websocket.go`, `main.go`).
- **Commands**: `:q` quits; `Esc` closes menus; help in `commands.go` (shortcuts vs text commands). Transient command results belong in the **banner** when short; longer lists (e.g. `:themes`) may use transcript System lines.
- **E2E**: same wire path for channel text and DMs when encryption on; files via keystore `EncryptRaw` / `DecryptRaw`. Do not log plaintext on send/decrypt paths.
Expand All @@ -26,17 +27,19 @@ Bubble Tea + Lipgloss TUI. Entry: `client/main.go`; split across `render.go`, `c
## Transcript rendering (`render.go`)

- **Word wrap**: `wrapStyledBlock` + `ansi.Wrap` at viewport width; preserves ANSI codes.
- **URLs**: `prepareURLWrapping` (non-breaking hyphens in hosts), `markURLsForWrap` / `applyURLMarkers` so hyperlink color and underline survive line breaks without styling continuation indent. **Click-to-open is not reliable for wrapped long URLs** in real terminals (manual mouse mapping + regex; headless tests do not cover lipgloss box chrome or emulator behavior). Workaround: copy URL from message. Tracked in [#103](https://github.com/Cod-e-Codes/marchat/issues/103). Planned fix: OSC 8 hyperlinks with full URL per segment when supported (`ROADMAP.md`).
- **URLs**: `prepareURLWrapping` (non-breaking hyphens in hosts), `markURLsForWrap` / `applyURLMarkers` so hyperlink color, underline, and OSC 8 sequences survive line breaks with the **full** href on every wrapped fragment. Mouse click-to-open (`findURLAtClickPosition`) remains as fallback when OSC 8 is unavailable. Copy from message still works everywhere.
- **Scroll input**: `activeScrollViewport` routes keyboard and `MouseWheelMsg` to help, DB menu, chat, or user list viewports; list modals (file picker, code-snippet language) handle wheel via `CursorUp`/`CursorDown`. Help and DB menu overlays (`overlayCapturesKeyboard`) swallow non-scroll keys so chat typing indicators, URL clicks, and read-receipt flush do not fire while browsing overlay content; `maybeFlushReadReceipt` only runs when the chat transcript viewport is active and at bottom.
- **Sort order**: `sortMessagesByTimestamp` / `messageLess` - persisted chat by `message_id`, server System (`message_id == 0`) by `created_at`, client-local System (negative `message_id`) after persisted chat.
- **Ephemeral System feedback**: `isTranscriptSystemNotice` / `isTranscriptSystemMessage` route command errors and one-line server replies (e.g. admin-only denial) to the **banner**; multi-line search, themes, and channel notices stay in the transcript. Negative `message_id` transcript notices are classified by **content** (`isTranscriptSystemNotice`), not ID sign alone. `pruneEphemeralSystemMessages` clears stale ephemeral lines on send or inbound persisted chat.
- **Client-local System lines**: negative `message_id` via `appendClientSystemMessage` for transcript notices only (with active `channel` set); short client usage/errors go to banner through `appendClientSystem`.

## Testing

- Inject `tea.Msg` in tests; no real terminal.
- `client/testmain_test.go`: `lipgloss.SetColorProfile(termenv.ANSI256)` so headless render/hyperlink tests emit real SGR sequences.
- `client/render_test.go`: URL wrap, hyperlink markers, system line severity, wrap width; URL click helpers (`buildTranscriptLineURLs`, `findURLAtTranscriptClick`) are headless only and do **not** validate real-terminal wrapped URL opens.
- `client/testmain_test.go`: `lipgloss.Writer.Profile = colorprofile.ANSI256` so headless render/hyperlink tests emit real SGR and OSC 8 sequences.
- `client/render_test.go`: URL wrap, OSC 8 hyperlink markers (`\x1b]8;;`), underline on wrapped segments, system line severity, wrap width; URL click helpers (`buildTranscriptLineURLs`, `findURLAtTranscriptClick`) are headless fallback-path coverage.
- `client/main_test.go`: DM/channel filters, unread, client system prune/sort, reconnect backoff, URL click hit/miss (single-line, headless), E2E search hint.
- `client/scroll_input_test.go`: scroll target selection, help viewport wheel handling, overlay input capture, read-receipt scoping, viewport dimension helpers
- `client/websocket_e2e_test.go`, `keystore_test.go`, `config_test.go`.
- See `testing-marchat` skill.

Expand Down
2 changes: 1 addition & 1 deletion .cursor/skills/server-marchat/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ App entry: `cmd/server/main.go`. Library: `server/` (hub, client, handlers, db,

## Admin

- TUI: `admin_panel.go`, `config_ui.go`.
- TUI: `admin_panel.go`, `config_ui.go` (Charm v2: `tea.View`, `KeyPressMsg`, bubbles setters). Admin panel enables `MouseModeCellMotion` and routes `MouseWheelMsg` for scrollable tabs and user/plugin tables.
- Web: `admin_web.go`, `admin_web.html`; `MARCHAT_SESSION_SECRET` (preferred), `MARCHAT_JWT_SECRET` deprecated; CSRF on mutating routes; login rate limit per IP.
- Trusted proxies: `MARCHAT_TRUSTED_PROXIES` for forwarded client IP.

Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The client is a standalone terminal user interface built with the Bubble Tea fra
- While a DM thread is open, the typing footer ignores channel or global typing (empty `recipient` on `typing` messages) so someone typing in the channel is not shown as typing inside the DM view; only DM-scoped typing for the open peer is shown. In channel view, typing is filtered to the active channel.
- DM unread and hidden-thread UI state is local client state in `dm_state.json` under the client config directory; opening a DM thread marks it read, and a hidden thread reappears on the next inbound DM from that user
- Automatic WebSocket reconnect with exponential backoff (capped at 30s; delay resets only after a successful connect, not on each retry attempt); on each successful connect (`wsConnected`), the reference client clears the in-memory transcript and related UI state before processing server history replay, so a server restart or network drop does not duplicate messages that were already on screen
- URL detection with underline styling; left-click attempts to open the URL under the cursor via manual mouse coordinate mapping (`findURLAtClickPosition` in `client/websocket.go`, `buildTranscriptLineURLs` in `client/render.go`). This path is **not reliable** when URLs wrap across transcript lines (common for long GitHub links in a narrow panel); the browser may open a truncated prefix. **Workaround:** copy the URL from the message text. A durable fix likely requires OSC 8 terminal hyperlinks (per-line full URL in the escape sequence) when the emulator supports them, with a tested fallback for terminals that do not; see `ROADMAP.md` and [#103](https://github.com/Cod-e-Codes/marchat/issues/103).
- URL detection with underline styling and **OSC 8 hyperlinks** on wrapped segments (`applyURLMarkers` in `client/render.go`; Lip Gloss v2 `Style.Hyperlink`). Terminals with OSC 8 support (e.g. Windows Terminal, iTerm) can Ctrl+click or open the **full** URL on every wrapped fragment. **Fallback:** left-click uses manual mouse coordinate mapping (`findURLAtClickPosition` in `client/websocket.go`, `buildTranscriptLineURLs` in `client/render.go`) and remains **unreliable** when URLs wrap across transcript lines; copy from the message when click-to-open fails ([#103](https://github.com/Cod-e-Codes/marchat/issues/103)).
- E2E send/decrypt paths do not log plaintext message content
- Multi-line input via Alt+Enter / Ctrl+J
- **Diagnostics**: `-doctor` and `-doctor-json` for environment, paths, and config checks (`internal/doctor`)
Expand Down
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Narrative notes by release. Per-file binaries and assets: [GitHub releases](http

On **`main`** only; not part of the latest tagged release until you tag and publish. Compare against the current tag on [GitHub releases](https://github.com/Cod-e-Codes/marchat/releases).

- **Client**: Word-wrap chat message bodies to the transcript viewport width (ANSI-aware). Reaction aliases `thumbsup` / `thumbsdown`; `:unreact`, `:thumbsup`, and `:thumbsdown` commands. When E2E is on and server search returns no matches, a `System` line explains that search matches stored ciphertext, not decrypted plaintext. **Fix:** long URLs break at path boundaries on resize (not mid-domain hyphens); hyperlink color and underline follow wrapped URL segments without styling continuation indent. Mouse click-to-open on transcript URLs is implemented (`findURLAtClickPosition`, `buildTranscriptLineURLs`) but **not reliable** for wrapped long URLs in real terminals (for example a GitHub commit link may open `https://github.com/Cod` instead of the full URL); copy the URL from the message until [#103](https://github.com/Cod-e-Codes/marchat/issues/103) is resolved. **Fix:** ephemeral `System` feedback (client usage errors, server command denials like admin-only, plugin one-liners) uses the banner instead of sticking in the transcript; scrollable lists (search, themes, channels) stay in the transcript. **Fix:** reconnect exponential backoff advances on failure (no longer reset each attempt); client transcript notices keep negative `message_id` until pruned, scoped to the active channel, and survive inbound chat; reactions and read receipts include channel; E2E paths no longer log plaintext.
- **Server**: Handshake replay queries up to 50 **visible** recent messages (SQL `LIMIT` after DM/public filter), on every connect including reconnect with no new traffic. `user_message_state` records `last_seen` only (`last_message_id` legacy/unused). `:cleardb` clears `user_message_state`. Postgres/MySQL CI smoke and WebSocket integration test cover visible replay SQL and second-connect wire replay. **Fix:** Postgres `boolean = integer` errors on `:search`, pin toggle, and pinned listing (shared dialect boolean helpers). **Fix:** MySQL `InitDB` parses DSNs with `mysql.Config`, forces `parseTime=true` (overrides explicit `parseTime=false`; logs a warning), and sets `Loc` to local time when unset. **Fix:** outbound messages always use the sender's joined channel (blocks cross-channel spoofing); typing, reactions, and read receipts are channel-scoped; non-admin unknown commands return `Unknown command` instead of admin-only text; `:backup` is SQLite-only with a clear error on Postgres/MySQL; legacy Postgres migrations use `BOOLEAN DEFAULT FALSE`.
- **Client**: Word-wrap chat message bodies to the transcript viewport width (ANSI-aware). Reaction aliases `thumbsup` / `thumbsdown`; `:unreact`, `:thumbsup`, and `:thumbsdown` commands. When E2E is on and server search returns no matches, a `System` line explains that search matches stored ciphertext, not decrypted plaintext. **Fix:** long URLs break at path boundaries on resize (not mid-domain hyphens); hyperlink color and underline follow wrapped URL segments without styling continuation indent. **Fix:** wrapped URL segments emit OSC 8 hyperlinks (Lip Gloss v2 `Style.Hyperlink`) with the full href on every fragment; mouse click-to-open remains as fallback on terminals without OSC 8 ([#103](https://github.com/Cod-e-Codes/marchat/issues/103)). **Charm v2:** Bubble Tea, Bubbles, and Lip Gloss migrated to `charm.land/*/v2` (declarative `tea.View`, `KeyPressMsg`, bubbles setters). **Fix:** mouse wheel scroll routes to the active viewport (help, DB menu, chat, user list) after v2 split `MouseWheelMsg` from click events; file picker and code-snippet language lists scroll on wheel. **Fix:** help and DB menu overlays block chat typing indicators, URL click handling, and read-receipt flush while browsing overlay content; read receipts only schedule when the chat transcript viewport is scrolled to the tail; DB menu viewport resizes with the terminal. **Fix:** v2 alt-screen `BackgroundColor` plus textarea chrome helpers restore main-branch layout (no black gutters, 3-line input, stable Users border) while keeping scroll routing and OSC 8 hyperlinks. **Fix:** ephemeral `System` feedback (client usage errors, server command denials like admin-only, plugin one-liners) uses the banner instead of sticking in the transcript; scrollable lists (search, themes, channels) stay in the transcript. **Fix:** reconnect exponential backoff advances on failure (no longer reset each attempt); client transcript notices keep negative `message_id` until pruned, scoped to the active channel, and survive inbound chat; reactions and read receipts include channel; E2E paths no longer log plaintext.
- **Server**: Handshake replay queries up to 50 **visible** recent messages (SQL `LIMIT` after DM/public filter), on every connect including reconnect with no new traffic. `user_message_state` records `last_seen` only (`last_message_id` legacy/unused). `:cleardb` clears `user_message_state`. Postgres/MySQL CI smoke and WebSocket integration test cover visible replay SQL and second-connect wire replay. **Fix:** admin panel TUI enables mouse mode and handles `MouseWheelMsg` for scrollable tabs and user/plugin tables. **Fix:** Postgres `boolean = integer` errors on `:search`, pin toggle, and pinned listing (shared dialect boolean helpers). **Fix:** MySQL `InitDB` parses DSNs with `mysql.Config`, forces `parseTime=true` (overrides explicit `parseTime=false`; logs a warning), and sets `Loc` to local time when unset. **Fix:** outbound messages always use the sender's joined channel (blocks cross-channel spoofing); typing, reactions, and read receipts are channel-scoped; non-admin unknown commands return `Unknown command` instead of admin-only text; `:backup` is SQLite-only with a clear error on Postgres/MySQL; legacy Postgres migrations use `BOOLEAN DEFAULT FALSE`.
- **Plugins**: **Fix:** plugin stdin writes are serialized so chat fan-out and command RPC cannot corrupt IPC lines.
- **Docs**: Coverage tables in **README** and **TESTING** drop per-package line counts; regeneration uses `go test -coverprofile` and `go tool cover -func` only. **ARCHITECTURE** and **PROTOCOL** document channel stamping, SQLite-only `:backup`, plugin stdin serialization, and client reconnect/E2E logging behavior; wrapped URL click-to-open limitation documented ([#103](https://github.com/Cod-e-Codes/marchat/issues/103)). Agent skills updated to match.
- **Tooling**: Project Agent skills under `.cursor/skills/`; always-on rules in `.cursor/rules/marchat.mdc`. `.gitignore` tracks shared rules and skills; removed legacy `.cursor/agents/` briefs. **Fix:** skills pipeline, em-dash cleanup in rules, and client transcript docs synced with URL wrap and ephemeral System line behavior.
- **Dependencies**: **github.com/jackc/pgx/v5** v5.10.0, **golang.org/x/crypto** v0.53.0, **golang.org/x/term** v0.44.0, **modernc.org/sqlite** v1.52.0 (SQLite 3.53.2).
- **Dependencies**: **charm.land/bubbletea/v2** v2.0.7, **charm.land/bubbles/v2** v2.1.0, **charm.land/lipgloss/v2** v2.0.4 (replaces Charm v1 stack). **github.com/jackc/pgx/v5** v5.10.0, **golang.org/x/crypto** v0.53.0, **golang.org/x/term** v0.44.0, **modernc.org/sqlite** v1.52.0 (SQLite 3.53.2).

## v1.2.0

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ go build -o marchat-client ./client
- Go 1.25.11 or later ([download](https://go.dev/dl/))
- Linux clipboard support: `sudo apt install xclip` (Ubuntu/Debian) or `sudo yum install xclip` (RHEL/CentOS)

**Terminal colors:** The server startup banner and the client’s pre-chat output (connection, E2E status, profile picker tags such as `[Admin]` / `[E2E]`, and auth prompts) use [lipgloss](https://github.com/charmbracelet/lipgloss) for emphasis. Set **`NO_COLOR=1`** (or **`NO_COLOR`**) in the environment to disable colors on plain stdout/stderr.
**Terminal colors:** The server startup banner and the client’s pre-chat output (connection, E2E status, profile picker tags such as `[Admin]` / `[E2E]`, and auth prompts) use [Lip Gloss v2](https://github.com/charmbracelet/lipgloss) (`charm.land/lipgloss/v2`) for emphasis. Set **`NO_COLOR=1`** (or **`NO_COLOR`**) in the environment to disable colors on plain stdout/stderr.

## Configuration

Expand Down
Loading