Commit Graph

162 Commits

Author SHA1 Message Date
ZhenYi
00a5369fe1 feat(frontend): Discord layout + AI Studio theme + Room Settings
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
Frontend:
- Add Discord-style 3-column layout (server icons / channel sidebar / chat)
- AI Studio design system: new CSS token palette (--room-* vars)
- Replace all hardcoded Discord colors with CSS variable tokens
- Add RoomSettingsPanel (name, visibility, AI model management)
- Settings + Member list panels mutually exclusive (don't overlap)
- AI models shown at top of member list with green accent
- Fix TS errors: TipTap SuggestionOptions, unused imports, StarterKit options
- Remove MentionInput, MentionPopover, old room components (废弃代码清理)

Backend:
- RoomAiResponse returns model_name from agents.model JOIN
- room_ai_list and room_ai_upsert fetch model name for each config
- AiConfigData ws-protocol interface updated with model_name

Note: RoomSettingsPanel UI still uses shadcn defaults (未完全迁移到AI Studio)
2026-04-18 16:59:36 +08:00
ZhenYi
aac32b1b92 fix(frontend): block selectionchange during MentionInput DOM updates
Root cause: when MentionInput updates innerHTML to reflect value changes,
the browser resets the selection to position 0. The selectionchange event
handler reads this wrong cursor position and sets ms.cursorOffset=0,
breaking mentionState calculation for the popover.

Fix:
- MentionInput sets window.__mentionBlockSelection=true before innerHTML
- Clears it via requestAnimationFrame after caret is restored
- selectionchange handler skips cursor reading when flag is set
2026-04-18 11:49:03 +08:00
ZhenYi
53b0b03716 fix(frontend): use selectionchange event for cursor tracking instead of per-render
The per-render tracking effect was reading DOM cursor position before
the browser had finished positioning the caret after innerHTML updates,
causing ms.cursorOffset to be set to the old position and overwriting
the deferred correct value.

Switch to document.selectionchange event which:
- Fires only when the user actually moves the cursor (arrow keys, mouse clicks)
- Does NOT fire during programmatic DOM updates
- Uses focusNode/focusOffset directly from the selection API

This completely eliminates the timing race between render effects and
programmatic cursor updates.
2026-04-18 11:42:43 +08:00
ZhenYi
4330325bfc fix(frontend): skip tracking effect update when caret is at end of text
Root cause: after onCategoryEnter sets value="@ai:", the MentionInput sync
effect sets innerHTML with the new content. The browser caret may be at
position 2 (not moved from the user's perspective), but prevCursorRef was
4. The tracking effect read DOM caret=2 and overwrote ms.setCursorOffset(4)
→ cursor jumped to position 2.

Fix: skip tracking effect update when:
- count (DOM caret) is at end of text (ms.value.length), AND
- prevCursorRef was also at end of text
This means the caret hasn't actually moved from the user's POV (just
positioned by the browser after innerHTML), so don't update state.
2026-04-18 11:39:16 +08:00
ZhenYi
126ffda4fe fix(frontend): skip cursor tracking effect during deferred cursor updates
Root cause: when onCategoryEnter scheduled a deferred setCursorOffset,
the tracking effect ran BEFORE setTimeout and read the OLD DOM cursor
position (still "@a" → position 2), overwriting the new cursor position
of 4 in a subsequent render.

Fix: add skipCursorTrackingRef flag. Set it before setTimeout fires,
clear it when setTimeout executes. The tracking effect checks the flag
and skips its update during the flush window.

Same fix applied to insertCategory imperative handle.
2026-04-18 11:27:37 +08:00
ZhenYi
fb09553b79 fix(frontend): defer cursor offset update in onCategoryEnter
When @category is selected, ms.setCursorOffset and ms.setValue were called
in the same event loop tick — the cursor sync effect later read the DOM
before the new mention value flushed, restoring the old caret position.

Defer ms.setCursorOffset via setTimeout so it fires after the DOM
reconciliation. Same fix applied to insertCategory imperative handle.
2026-04-18 11:24:01 +08:00
ZhenYi
5103d99a00 fix(frontend): rewrite MentionInput sync to prevent onChange overwriting mention draft
The root cause: handleMentionSelect set draft to mention HTML, but the
subsequent MentionInput.onInput event fired with plain text (after the
programmatic DOM insert) and overwrote the draft.

Solution: replace pendingSyncRef trick with a clean isUserInputRef flag.
- useEffect: if getPlainText(el) === value, DOM already correct (skip).
  Mark isUserInputRef = false so next useEffect skips caret restore.
- handleInput: always set isUserInputRef = true before calling onChange.

This eliminates the pendingSyncRef/__mentionSkipSync global flag mess
entirely.
2026-04-18 11:20:48 +08:00
ZhenYi
4ace651c6f fix(frontend): pass selected item to doInsert on Enter key and fix list keys
- handleSelectRef.current() was called without argument → suggestion was
  undefined → suggestion.type threw. Now passes the selected item.
- key prop moved to outer map wrapper div so React can diff the list
  correctly. Inner SuggestionItem/CategoryHeader no longer need keys.
2026-04-18 11:16:04 +08:00
ZhenYi
989a4117db fix(frontend): fix useMemo deps syntax and replace stale ref reads in render body
useMemo received deps as trailing comma-separated args instead of array.
Replace all render-body reads of mentionStateRef.current with reactive
mentionState from useMemo. Keep ref for event listener closures only.
2026-04-18 11:14:10 +08:00
ZhenYi
434a3595eb fix(frontend): mention popover respects shouldBelow for above/below placement
ShouldBelow was computed but ignored — popover always rendered below the
cursor. Now uses shouldBelow correctly to render above when insufficient
space below. Also removes unused cRect clone measurement.
2026-04-18 11:11:39 +08:00
ZhenYi
3a164f6dcc fix(frontend): restore missing useMemo import in MentionPopover 2026-04-18 11:09:53 +08:00
ZhenYi
a834564c0f fix(frontend): show mention popover when @ is typed
Effect watches mentionState and toggles showMentionPopover accordingly.
Without this, ms.setShowMentionPopover was never called in onChange,
so the popover never appeared.
2026-04-18 11:09:17 +08:00
ZhenYi
8d3ecca0bc fix(frontend): restore missing cn import in RoomChatPanel 2026-04-18 11:07:15 +08:00
ZhenYi
f152aaeed9 fix(frontend): 4 mention chain bugs — cursor tracking, caret placement, undefined refs
- Fix undefined setDraftAndNotify (replaced with onDraftChangeRef + ms.setValue)
- Add DOM→ms.cursorOffset tracking on every render via TreeWalker
- Fix mention insertion caret placement: skip MentionInput sync-effect caret
  restoration when parent sets value (via __mentionSkipSync window flag)
- Remove unused _draftTick state

Bug #1: setDraftAndNotify referenced but never defined → runtime crash
Bug #2: ms.cursorOffset never updated from DOM → popover always at wrong position
Bug #3: mention insertion saved pre-insertion caret, restored on longer HTML → cursor inside mention tag
Bug #4: _draftTick declared but state value never read → dead code
2026-04-18 01:34:22 +08:00
ZhenYi
168fdc4da8 refactor(frontend): rewrite mention system with clean state architecture
Replace module-level refs and circular dependencies with a centralized
mention state hook (useMentionState) and shared types (mention-types).

- Introduce mention-types.ts for shared types (avoid circular deps)
- Introduce mention-state.ts with useMentionState hook (replace global refs)
- Rewrite MentionInput as contenteditable with AST-based mention rendering
- Rewrite MentionPopover to use props-only communication (no module refs)
- Fix MessageMentions import cycle (mention-types instead of MentionPopover)
- Wire ChatInputArea to useMentionState with proper cursor handling
2026-04-18 01:26:35 +08:00
ZhenYi
d2935f3ddc fix(frontend): repair MentionInput contenteditable implementation
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
- MentionInput: use forwarded ref, internal state ref, pendingSyncRef
  flag to prevent value↔DOM sync cycles; getContentText walks both
  text nodes and non-editable mention spans to preserve mention length
- RoomChatPanel: replace cursorRef with getCaretOffset()/setCaretOffset()
  helpers, remove stale cursorRef.current references throughout
- MentionPopover: update textareaRef type to HTMLDivElement,
  handleSelect uses TreeWalker to read from contenteditable div,
  positioning effect always uses TreeWalker for text measurement
2026-04-18 01:17:04 +08:00
ZhenYi
b7328e22f3 feat(frontend): render mentions as styled buttons in input and messages
Some checks are pending
CI / Frontend Build (push) Blocked by required conditions
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
- MentionInput: contenteditable div that renders all mention types as
  emoji+label pill spans (🤖 AI, 👤 User, 📦 Repo, 🔔 Notify)
- Single-backspace deletes entire mention at cursor (detects caret
  at mention start boundary)
- Ctrl+Enter sends, plain Enter swallowed, Shift+Enter inserts newline
- Placeholder CSS via data-placeholder attribute
- MessageMentions: emoji button rendering extended to all mention types
  (user, repository, notify) with click-to-insert support
- Rich input synced via cursorRef (no stale-state re-renders)
2026-04-18 01:06:39 +08:00
ZhenYi
b96ef0342c feat(room): render AI mentions as 🤖 button with click-to-insert action
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
AI mentions now render as "🤖 AI: name" buttons instead of plain text.
Clicking a 🤖 button inserts the @ai:mention into the message input at
the cursor position, enabling users to summon that AI model into the conversation.

Implementation:
- MessageMentions renders AI mentions as styled buttons with 🤖 icon
- Click dispatches 'mention-click' CustomEvent on document
- ChatInputArea listens and inserts the mention HTML at cursor
2026-04-18 00:57:27 +08:00
ZhenYi
3a24022972 fix(room): load model list inside fetchRoomAiConfigs so AI names are always resolved
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
Previously availableModels was fetched in parallel with roomAiConfigs, so
the first call to fetchRoomAiConfigs had empty availableModels and all AI
configs showed model IDs instead of names.

Now fetchRoomAiConfigs loads the model list itself when called, guaranteeing
that model names are always available.
2026-04-18 00:55:18 +08:00
ZhenYi
7be2f4eb61 fix(deploy): simplify ingress by removing static paths
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
The SPA assets are embedded in the app binary, so no separate
static file serving is needed.
2026-04-18 00:51:52 +08:00
ZhenYi
17e878c8b8 fix(room): fix Enter-on-category via React state update instead of DOM manipulation
Problem: dispatchEvent('input') doesn't trigger React's onChange in React 17+.

Solution: pass onCategoryEnter callback from ChatInputArea to MentionPopover.
When Enter is pressed on a category, MentionPopover calls onCategoryEnter(category)
which directly calls React setState (onDraftChange, setCursorPosition,
setShowMentionPopover) inside ChatInputArea, properly triggering re-render.
2026-04-18 00:51:14 +08:00
ZhenYi
14de80b24b fix(room): Enter on category navigates into it, not out of popover
When a category (Repository/User/AI) is selected and Enter is pressed,
append ':' to the textarea value to trigger the next-level item list.
E.g. '@ai' + Enter → '@ai:' → shows AI model items.
2026-04-18 00:42:08 +08:00
ZhenYi
245384ef50 fix(room): handle Enter/Tab mention selection directly in ChatInputArea.handleKeyDown
Previous approach used a native addEventListener in MentionPopover to handle
Enter, but it wasn't firing reliably due to complex event ordering.

New approach: ChatInputArea.handleKeyDown detects @ mention directly from
the DOM (not React state), reads the selected item from module-level
mentionVisibleRef/mentionSelectedIdxRef, and performs the insertion directly.
This completely bypasses the native listener timing issues.
2026-04-18 00:39:06 +08:00
ZhenYi
b8a61b0802 fix(room): make handleSelect read DOM directly instead of stale props
Root cause: MentionPopover's native keydown listener fires before React
state updates, so handleSelect read stale inputValue/cursorPosition props
and silently returned early.

Fix: handleSelect now reads textarea.value/selectionStart directly from
the DOM, matching the approach already used in ChatInputArea's
handleMentionSelect. No more stale closure.
2026-04-18 00:35:21 +08:00
ZhenYi
3cd5b3003c fix(room): fix mention Enter key by reading textarea DOM directly
Problem: showMentionPopover state is stale when handleKeyDown fires
immediately after handleChange (both in same event loop), causing Enter
to be silently swallowed.

Solution:
- Read textarea.value directly in handleKeyDown to detect @ mentions
- Module-level refs (mentionSelectedIdxRef, mentionVisibleRef) share
  selection state between MentionPopover and ChatInputArea
- handleMentionSelect reads DOM instead of relying on props state
2026-04-18 00:25:03 +08:00
ZhenYi
13f5ff328c fix(room): hoist mentionConfirmRef to module scope to fix TDZ error
ChatInputArea is defined before RoomChatPanel in the file, so its JSX
runs before mentionConfirmRef is declared inside RoomChatPanel. Moving the
ref to module level ensures it's initialized before either component renders.
2026-04-18 00:17:47 +08:00
ZhenYi
14bcc04991 fix(room): update keyboard shortcuts — Ctrl+Enter sends, Shift+Enter newlines, Enter only for mention select
- Ctrl+Enter: send message (was plain Enter)
- Shift+Enter: insert newline (textarea default, passes through)
- Enter alone: only triggers mention selection when popover is open, otherwise does nothing
- Update MentionPopover footer/header hints to reflect new shortcuts
2026-04-18 00:11:13 +08:00
ZhenYi
9b966789fd fix(room): resolve mention IDs to display names when rendering messages
- Pass members/repos/aiConfigs lists to MessageContentWithMentions
- Add resolveName() that looks up ID → display name per mention type
- RoomMessageBubble now resolves user/repository/AI mention UIDs to real names
2026-04-18 00:10:12 +08:00
ZhenYi
a9fc6f9937 feat(room): redesign MentionPopover with modern UI/UX
Visual improvements:
- Glassmorphism backdrop blur and refined shadows
- Color-coded categories: user(sky), repository(violet), AI(emerald)
- Gradient backgrounds and smooth transitions
- Custom avatar icons for each mention type

Interaction enhancements:
- Search text highlighting with yellow background
- Auto-scroll selected item into view
- Selection indicator dot and left border accent
- Keyboard navigation (↑↓) with visual feedback

Components:
- CategoryHeader: icon + label with color theme
- SuggestionItem: avatar + highlighted text + category badge
- LoadingSkeleton: Shimmer loading state
- EmptyState: Illustrated empty/loading states

Uses ScrollArea, Avatar, Skeleton from design system
2026-04-18 00:02:09 +08:00
ZhenYi
aacd9572d1 fix(room): replace useMemo+categories with plain const to fix SWC parser error 2026-04-17 23:53:31 +08:00
ZhenYi
9246a9e6ab fix(room): move eslint-disable comment inside array to fix SWC syntax error 2026-04-17 23:52:27 +08:00
ZhenYi
f9a3b51406 perf(room): optimize MentionPopover with caching, stable refs, and loading states
- Position caching: skip recalculation when text+cursor unchanged
- TempDiv reuse: cached DOM element on textarea, created once
- Stable refs pattern: avoid stale closures in keyboard handler
- Auto-selection: reset to first item on category/list change
- Loading states: reposLoading + aiConfigsLoading wired from context
2026-04-17 23:48:26 +08:00
ZhenYi
26682973e7 feat(room): redesign mention system with AST-based format
Backend:
- Add MENTION_TAG_RE matching new `<mention type="..." id="...">label</mention>` format
- Extend extract_mentions() and resolve_mentions() to parse new format (legacy backward-compatible)

Frontend:
- New src/lib/mention-ast.ts: AST types (TextNode, MentionNode, AiActionNode),
  parse() and serialize() functions for AST↔HTML conversion
- MentionPopover: load @repository: from project repos, @ai: from room_ai configs
  (not room members); output new HTML format with ID instead of label
- MessageMentions: use AST parse() for rendering (falls back to legacy parser)
- ChatInputArea: insertMention now produces `<mention type="user" id="...">label</mention>`
- RoomParticipantsPanel: onMention passes member UUID to insertMention
- RoomContext: add projectRepos and roomAiConfigs for mention data sources
2026-04-17 23:43:26 +08:00
ZhenYi
0ea6440ea3 fix(deploy): add proxy-http-version annotation for WebSocket support 2026-04-17 23:18:00 +08:00
ZhenYi
6431709669 fix(room): show model name instead of UID in settings panel
- Load model list on settings panel mount so names are always available.
- SelectValue now displays the selected model's name by looking up
  availableModels, falling back to UID if name not found.
- Existing AI configs list also shows model name instead of raw UID.
2026-04-17 23:17:56 +08:00
ZhenYi
5ff45770ec fix(room): fix model/ai list response parsing in RoomSettingsPanel
The SDK wraps API responses as { data: { code, message, data: [...] } }.
Code was incorrectly accessing resp.data['200'] which doesn't exist.
Fix to use resp.data.data to reach the actual array.
2026-04-17 23:15:14 +08:00
ZhenYi
4f1ea95b58 fix(room): add missing stream column to room_ai table
Create migration m20260417_000001_add_stream_to_room_ai that adds
the `stream BOOLEAN NOT NULL DEFAULT true` column to room_ai.
The model definition includes this field but the original migration
was missing it.

Also format libs/api/room/ws.rs and add gitdata.ai to allowed
WS origins.
2026-04-17 23:09:55 +08:00
ZhenYi
a171d691c6 fix(room): align ReactionGroup types with frontend and guard reaction update handler
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
- Fix ReactionGroup.count: i64 -> i32 and users: Vec<Uuid> -> Vec<String>
  to match frontend ReactionItem (count: number, users: string[]).
  Mismatched types caused the WS reaction update to silently fail.
  Also update ReactionItem in api/ws_types.rs to match.
- Add activeRoomIdRef guard in onRoomReactionUpdated to prevent stale
  room state from processing outdated events after room switch.
- Switch from prev.map() to targeted findIndex+spread in onRoomReactionUpdated
  to avoid unnecessary array recreation.
2026-04-17 23:00:52 +08:00
ZhenYi
047782e585 fix(room): handle reaction updates in onRoomMessage WS handler
Root cause: publish_reaction_event sends a RoomMessageEvent (not
reaction_added) with reactions field set. The onRoomMessage handler
previously returned prev immediately when a duplicate ID was found,
skipping the reaction update entirely.

Fix:
- Add reactions field to RoomMessagePayload so TypeScript knows it's there
- When a duplicate message ID is found AND payload carries reactions,
  update the existing message's reactions field instead of ignoring
- ReactionItem and ReactionGroup have identical shapes, so assignment works
2026-04-17 22:37:20 +08:00
ZhenYi
0cbf6d6aa1 fix(room): accept comma-separated message_ids string in batch reaction endpoint 2026-04-17 22:28:24 +08:00
ZhenYi
7152346be8 fix(room): make reaction Popover controlled so it closes after select 2026-04-17 22:16:40 +08:00
ZhenYi
a1ddb5d5bc fix(room): add HTTP batch reactions endpoint and clean up dead code
Backend:
- Add reaction_batch handler: GET /api/rooms/{room_id}/messages/reactions/batch?message_ids=...
- Register route in libs/api/room/mod.rs
- Backend already had message_reactions_batch service method, just needed HTTP exposure

Frontend:
- Add ReactionListData import to room-context.tsx
- Fix thisLoadReactions client type (was using broken NonNullable<ReturnType<>>)
- Remove unused oldRoomId variable
- Delete unused useRoomWs.ts hook (RoomWsClient has no on/off methods)
- Remove unused EmojiPicker function and old manual overlay picker from RoomMessageBubble
- Remove unused savedToken variable in room-ws-client
2026-04-17 22:12:10 +08:00
ZhenYi
2f1ed69b31 fix(room): restore useState import removed in refactor 2026-04-17 22:02:55 +08:00
ZhenYi
ef1adb663d fix(room): replace manual emoji picker positioning with Popover
Manual getBoundingClientRect positioning caused the picker to appear at
the far right of the room and shift content. Replaced with shadcn
Popover which handles anchor positioning, flipping, and portal rendering
automatically.
2026-04-17 21:50:50 +08:00
ZhenYi
4767e1d692 fix(room): IDB load no longer waits for WS connection
loadMore(null) fires immediately — cached messages render instantly.
WS connect runs in parallel; subscribeRoom and reaction batch-fetch
use WS-first request() which falls back to HTTP transparently.
2026-04-17 21:45:55 +08:00
ZhenYi
44c9f2c772 fix(room): ensure WS connect before subscribe; reactions load for IDB path
- setup() now awaits client.connect() before subscribeRoom and loadMore,
  ensuring the connection is truly open so WS is used for both
- subscribeRoom / reactionListBatch: already WS-first via RoomWsClient.request()
- IDB paths (initial + loadMore) now call thisLoadReactions to batch-fetch
  reactions via WS with HTTP fallback, fixing the missing reactions bug
2026-04-17 21:44:45 +08:00
ZhenYi
50f9cc40fe fix(room): load reactions for IDB-cached messages via WS-first fallback
- thisLoadReactions helper: batch-fetches reactions for loaded messages
  via WS (RoomWsClient.request() does WS-first → HTTP fallback automatically)
- Called after both IDB paths (initial load + loadMore) so reactions are
  populated even when messages come from IndexedDB cache
- Also deduplicated API-path reaction loading to use the same helper
2026-04-17 21:43:47 +08:00
ZhenYi
b70d91866c fix(room-ws): try reconnect with existing token before requesting new
On auto-reconnect (scheduleReconnect), attempt connection with the stored
wsToken first. If the WS closes immediately (server rejected the token),
fall back to fetching a fresh token and retrying. Only requests a new
token when the existing one fails or when connect() is called manually
with forceNewToken=true.
2026-04-17 21:40:07 +08:00
ZhenYi
309bc50e86 perf(room): prioritize IndexedDB cache for instant loads
- Remove clearRoomMessages on room switch: IDB cache now persists across
  visits, enabling instant re-entry without API round-trip
- Increase initial API limit from 50 → 200: more messages cached upfront
- Add loadOlderMessagesFromIdb: uses 'by_room_seq' compound index to
  serve scroll-back history from IDB without API call
- loadMore now tries IDB first before falling back to API
2026-04-17 21:36:55 +08:00
ZhenYi
bab675cf60 perf(room): increase virtualizer overscan to 30 for smoother scrolling 2026-04-17 21:31:36 +08:00