- Rewrite DiscordChannelSidebar with @dnd-kit drag-and-drop:
rooms are sortable within categories; dragging onto a different
category header assigns the room to that category
- Add inline 'Add Category' button: Enter/Esc to confirm/cancel
- Wire category create/move handlers in room.tsx via RoomContext
- Fix onAiStreamChunk to accumulate content properly and avoid
redundant re-renders during AI streaming (dedup guard)
- No backend changes needed: category CRUD and room category update
endpoints were already wired
Converts AppEmail from blocking sync sends to a background worker via
mpsc channel, adds SMTP pool tuning (min_idle 5, max_size 100), and
3-retry backoff on send failures.
The login function calls auth_2fa_status before set_user(user.uid), so
context.user() returns None and causes Unauthorized error on subsequent
logins after logout. Extracts auth_2fa_status_by_uid as an internal
helper accepting a Uuid, preserving the context-based wrapper for API
endpoints that require an authenticated user.
Add the second half of the password reset flow: /password/confirm API
endpoint with token validation, transactional password update, and a
frontend confirm-password-reset-page with proper error handling for
expired/used tokens. Updates generated SDK/client bindings.
Add AI-accessible tools for reading structured files (CSV, JSON/JSONC,
Markdown, SQL) and searching repository content (git_grep). Also adds
git_blob_get to retrieve raw blob text content with binary detection.
Deferred: Excel, Word, PDF, PPT modules are stubbed out due to library
API incompatibilities (calamine 0.26, lopdf 0.34, quick-xml 0.37).
- Add showSearch state and RoomMessageSearch panel (420px slide-in)
- Search button now toggles panel and highlights when active
- Clicking a search result scrolls to the message and closes panel
- Add msg-{id} anchors on message containers for scroll-into-view
InvitationResponse was missing project_name and invited_by_username fields,
causing /invitations accept to redirect to /project/undefined.
Now populated via async from_model() with batch DB lookups.
- Refactor room-context.tsx with improved WebSocket state management
- Enhance room-ws-client.ts with reconnect logic and message handling
- Update Discord layout components with message editor improvements
- Add WebSocket universal endpoint support in ws_universal.rs
- Add /invitations route with dedicated page and sidebar button
- Display both project invitations (accept/decline) and workspace invitations (accept only)
- Merge and sort both invitation types by most recent first
- Export new SDK functions: workspaceMyInvitations, workspaceAcceptInvitationBySlug
- Add workspace_my_pending_invitations() for listing pending invites
- Add workspace_accept_invitation_by_slug() to accept by slug without token
- Register new routes: GET /workspaces/me/invitations, POST /workspaces/invitations/accept-by-slug
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)
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
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.
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.
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.
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.
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.
- 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.
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.
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.
Effect watches mentionState and toggles showMentionPopover accordingly.
Without this, ms.setShowMentionPopover was never called in onChange,
so the popover never appeared.
- 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
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
- 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
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
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.
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.
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.
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.
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.
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
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.
- 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