This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/TODO-0.23.2.md
2026-03-06 23:17:03 +00:00

20 KiB
Raw Blame History

TODO — v0.23.2

Working checklist. Covers remaining v0.23.1 multi-user scope plus deferred v0.22.8 surface integration and the channel persistence bug. ROADMAP and CHANGELOG updated when work is complete.


Already Shipped (in 0.23.0 / 0.23.1 changesets)

Backend foundation is in place. These are done and should not be re-implemented.

  • Migration 016: ai_mode, topic on channels; user_presence table; channel type constraint extended to dm, channel
  • ai_mode guard in completion.gooff returns 403, mention_only without @mention returns {status: delivered}
  • resolveMention() resolves users (steps 34: exact + prefix on users.username, self-mention blocked)
  • User @mention in completion handler — skips AI, attempts NotifyUserMention via interface cast (hub method not yet implemented — silently no-ops)
  • Presence endpoints: POST /presence/heartbeat (upsert), GET /presence?users=... (90s threshold query)
  • Folders: full CRUD handler (handlers/folders.go), wired routes
  • CreateChannel accepts type field (default direct)
  • ListChannels supports ?types=dm,channel multi-filter
  • Three-section sidebar in chat.html (Projects / Channels / Chats)
  • renderChannelsSection() in ui-core.js (# / person icons, unread badges, online dots, active state)
  • selectChannel(), newChannelOrDM(), loadChannels() in projects-ui.js
  • Folder UI: create, rename, delete, context menu, drag chats
  • Chat list filters out type=channel and type=dm
  • API methods: createChannel(title,model,sp,type), listSidebarChannels(), presenceHeartbeat()
  • persona_groups + persona_group_members tables (migration 004)
  • PersonaGroup / PersonaGroupMember model structs
  • Channel participants: CRUD handler, auto-created on channel create

Bug Fixes

  • Channel persistence through refresh. loadChannels() read resp.channels but ListChannels returns a paginatedResponse with data key. Channels created in-session appeared (pushed to App.channels client-side) but vanished on reload because the API response was always parsed as empty. Fix: resp.channelsresp.data in projects-ui.js.

  • Folder drag-and-drop completely broken. Three compounding issues: (a) loadChats() never mapped c.folderfolderId — all chats rendered as unfiled regardless of DB state. (b) Folder groups rendered "Drop chats here" but had zero drag event handlers (ondragover/ondrop missing). (c) No drop zone existed for unfiling a chat or removing from a project — the Chats section body had no drag handlers. Fixes: added folderId: c.folder || null to loadChats() map; added ondragover/ondragleave/ondrop to .sb-folder-group divs in renderChatList(); added onChatSectionDrop handler to #sbBodyChats (handles both unfile-from-folder and remove-from- project); added onFolderDrop and moveChatToFolder functions; added "Move to folder" / "Remove from folder" to chat context menu; added drag-over CSS for folders and section body.

  • DM creation 404 on /api/v1/users. Route didn't exist. Added GET /api/v1/users/search?q= endpoint returning id, username, display_name for approved users (max 20, excludes caller). Registered on protected group (any authed user). Rewrote DM creation UI from text input to lazy search modal — shows all users on open, filters as you type with 200ms debounce, click to select and create DM.

  • No unfiled drop zone when folders exist. When folders were present, folder groups consumed all space in #sbBodyChats, leaving no bare target to drag a chat out of a folder. Added .sb-unfiled-zone wrapper around unfiled chats with its own drag handlers. Shows "Drop here to unfile" hint when zone is empty. onUnfiledDrop handles both unfile-from-folder and remove-from-project.

  • No channel CRUD. Channel sidebar items had no context menu or management affordances. Added: right-click context menu with Rename / Set topic (channels only) / Delete. Inline ✕ delete button on hover (same pattern as chat items). renameChannel() uses _showCreationDialog with pre-filled value. deleteChannel() with confirm dialog removes from App.channels + App.chats, clears active if deleted channel was selected.

  • Drag-over highlight persists. onChatDragEnd only cleared .project-group.drag-over — missed folders, unfiled zone, and section body. Fixed to document.querySelectorAll('.drag-over') so all drop targets are cleaned up when a drag ends anywhere.

  • Channel ⋯ menu inconsistency. Channels used ✕ delete button + right-click only, while folders used ⋯ hover button. Replaced channel ✕ with .sb-ch-menu ⋯ button matching the folder pattern (hidden, shown on hover, triggers context menu on click). Both channels and folders now have ⋯ hover + right-click.

  • Folder delete silently spills chats. deleteFolder() always unfiled contained chats with no user input. Replaced with a three-button modal when the folder has chats: "Keep chats" (unfile and preserve), "Delete chats too" (hard-delete all contained chats), or Cancel. Empty folders get a simple confirm. Delete-all path iterates chats, calls API.deleteChannel() for each, clears active state if needed, then removes the folder.

  • Folder ⋯ button shifts collapse arrow. The ⋯ button used display:none/block which caused reflow on hover, shifting the arrow left. Fixed: swapped DOM order (⋯ before arrow), changed to visibility:hidden/visible so space is always reserved. Applied same fix to channel ⋯ button. Arrow gets fixed width with flex-shrink:0.

  • Channels STILL not surviving refresh (round 2). The unread count subquery in ListChannels referenced cp.last_read_message_id — a column added by migration 017. If 017 hasn't been applied (fresh DB only has through 016), the query fails and returns zero channels. Additionally, the $1 parameter was reused in Postgres (subquery + WHERE) which would break SQLite's positional ? binding. Fix: rewrote unread subquery to use last_read_at (exists since migration 005). Split $1 reuse into $1 (subquery) and $2 (WHERE), with args providing userID twice. MarkRead also fixed to use last_read_at as primary, last_read_message_id as best-effort (silently fails if 017 not applied).

  • Settings Models section empty. Template models section fell through to the generic settingsDynamic div, but loadUserModels() wrote to #userModelList which didn't exist. Added dedicated template sections for models and teams.

  • CI: TestIntegration_Messages_CRUD 500. ListMessages JOIN used u.avatar but the column is avatar_url in the users table. Query failed on SQLite (Postgres would fail too). Same bug in treepath/path.go resolveSenderInfo(). Fix: u.avataru.avatar_url in both locations.


Design Decisions (resolved)

Unified active conversation. App.currentChatId and App.currentChannelId merge into App.activeConversation = { id, type } where type is direct|dm|group|channel. The send path, model bar, input state, streaming, session restore, and URL sync all key off activeConversation.id. The type discriminator drives ai_mode checks, context banner visibility, and participant rendering. Refactor touches selectChat(), selectChannel(), sendMessage(), session restore in app.js, and active-highlight in sidebar renderers.

DM deduplication. One channel per participant pair. CreateChannel for type=dm checks for an existing DM with the same two participants before creating. Display name is the other party's name (from the caller's perspective). For future 3+ human DMs: comma-join up to 2 names + "and N others".

No LLMs in DMs. DMs are human-to-human by definition. ai_mode is mention_only — you can @mention a persona for a one-off response, but personas are not participants. If you want persistent AI in a conversation with another human, that's a Group Chat (type=group). The taxonomy stays clean.

Post-creation participant mutation. Users and personas can be added or removed from any channel/group after creation. Participant CRUD endpoints already exist. Constraints: DMs cannot drop below 2 human participants; groups cannot drop their last persona (becomes a DM at that point — block with error, not silent type coercion).

Channel deletion vs archival. Governed by channel_retention.mode:

  • flexible (default): delete or archive freely, user's choice.
  • retain: archive only. Delete button becomes "Archive" everywhere. DELETE /channels/:id returns 403 for non-admins. Admins purge from the admin panel, subject to retention age floor.

See "Retention Policy" section below for the full lifecycle.


1 · Unified Active Conversation

Prerequisite refactor. Do this first — everything else builds on it.

  • App.activeConversation object. Replace App.currentChatId and App.currentChannelId with App.activeConversation = { id, type } (null when nothing selected). Update app-state.js. Added activeId getter, activeType getter, setActive(id,type) method, and getActiveChat() helper.
  • selectChat() migration. Sets App.activeConversation = { id: chatId, type: chat.type }. Session restore reads/writes cs-active-conversation (JSON).
  • selectChannel() migration. Sets App.activeConversation = { id, type: ch.type }. Same session storage key. Now also ensures conversation object in App.chats for message caching and maps messages into standard shape.
  • Send path. sendMessage() reads App.activeId for the channel ID. No behavior change — the backend already handles all channel types on the same completion endpoint.
  • Model bar, input state, streaming. All currentChatId references → App.activeId. All App.chats.find(...) for active chat → App.getActiveChat(). 12 files updated.
  • Sidebar active highlight. Both renderChatList() and renderChannelsSection() check App.activeId for the active class. Selecting either clears the other.
  • URL sync. history.replaceState uses /chat/${activeConversation.id} regardless of type. The Go route handler already loads any channel by ID.
  • Session restore. Reads cs-active-conversation JSON, checks both App.chats and App.channels arrays, calls selectChat() or selectChannel() as appropriate.
  • newChannelOrDM() integration. Now pushes to both App.channels (sidebar) and App.chats (message cache), and sets the new channel as active.

2 · Channel + DM Plumbing

  • NotifyUserMention on events hub. Replaced broken gin context interface cast with direct h.hub.SendToUser(). Delivers user.mentioned event with channel_id, from_user, content preview. Added user.mentioned to event route table as DirToClient. Added truncateContent() helper.
  • DM creation flow. Sidebar "+" shows choice dialog (New Channel / New DM). DM flow uses _showCreationDialog with username input, looks up user by username, creates type=dm channel with participants: [targetId]. Sets ai_mode=mention_only by default.
  • DM dedup guard. CreateChannel for type=dm queries channel_participants for existing DM between the two users. Returns existing channel (200) instead of creating duplicate (201). Self-DM blocked with 400.
  • Channel context banner. #channelContextBanner div between chat header and messages. UI.updateContextBanner() shows/hides based on App.activeConversation.type. DM shows partner name + @mention hint. Group shows leader/all routing hint. Channel shows ai_mode + topic. CSS in chat.css.
  • ai_mode and topic in API response. Added AiMode and Topic fields to channelResponse. ListChannels SELECT and Scan updated. CreateChannel INSERTs ai_mode (defaults to mention_only for DMs, auto otherwise).
  • DM participant auto-creation. CreateChannel adds all req.Participants as member participants alongside the creator (owner). Works for both Postgres and SQLite.
  • Unread counts. Add last_read_message_id and last_read_at columns to channel_participants. Backend computes unread count per channel per user. listSidebarChannels response includes unread_count. Mark-read on select.

3 · @mention UX for Users

  • Autocomplete includes users. channel-models.js onInput() builds candidates from App.models only. Add App.users list (fetched once on startup from GET /api/v1/users or lightweight endpoint). Merge into candidates with a person icon and different accent color. Resolution order in autocomplete: personas first, then users, then models (mirrors backend resolveMention).
  • User mention pill styling. @username pills render with a different background color (e.g. blue/teal vs persona accent) and person icon to distinguish human from AI mentions.
  • Notification on mention. user.mentioned WS event handler in chat.js increments unread badge on channel sidebar item and shows a toast with content preview.

4 · Presence

  • Client heartbeat. 30s setInterval calling API.presenceHeartbeat(). Start on app init, pause on visibilitychange hidden, resume on visible.
  • Presence on channel load. When rendering the Channels section, collect DM partner IDs and call GET /api/v1/presence?users=... to populate App.presence. Wire into renderChannelsSection() online dot.
  • Presence WebSocket events. Hub broadcasts presence.changed { userId, status } to sessions in shared channels. Frontend updates App.presence and re-renders the relevant sidebar item (not full re-render).

5 · Persona Groups + Group Chat

  • Persona group CRUD endpoints. Wire persona_groups and persona_group_members to handlers. Routes: GET/POST /api/v1/persona-groups, GET/PUT/DELETE /api/v1/persona-groups/:id, POST/DELETE /api/v1/persona-groups/:id/members. is_leader flag on members.
  • "New Group Chat" flow. UI in sidebar "+" or separate button. Option A: pick from saved persona group template. Option B: ad-hoc persona picker (checkboxes). Creates type=group channel, adds persona participants, sets leader via is_leader. Humans and personas can be added post-creation via participant CRUD.
  • Group leader default response. In completion.go, when channel type=group and no @mention in content, resolve the leader persona from persona_group_members WHERE is_leader (via channel_participants → group membership). Use leader's model/config/system prompt for the completion.
  • @all fan-out. When @all is detected in message content, route to every persona participant. Reuse existing multiModelStream path or sequential chain. Depth-1 only (responses from @all do not trigger further chains).
  • Participant mutation guards. DMs: block removal if it would drop below 2 human participants. Groups: block removal of last persona (return 400 with clear error message, not silent type coercion).

6 · Message Attribution

  • Human sender display. Messages from other human participants show their avatar + display name instead of "You". Requires the message response to include sender_name, sender_avatar (or participant_type=user + participant_id that the frontend resolves from a user cache).
  • Persona sender display. Already works for streaming via avatar resolution. Verify it works for loaded history in channel context (not just direct chats).

7 · Channel Lifecycle

  • Archive action. Context menu on channel/DM sidebar items: "Archive". Sets is_archived = true, archived_at = NOW() via PUT /channels/:id. Archived channels move to a collapsed "Archived" sub-section at the bottom of the Channels list (or hidden entirely with a "Show archived" toggle).
  • Delete action (gated). When channel_retention.mode is flexible (default): context menu shows "Delete". Hard-deletes the channel and all messages. When retain: context menu shows "Archive" only. No delete option for non-admins.
  • Admin purge. Admin panel gets a "Channels" section or row in an existing section showing archived channels with age. "Purge" button hard-deletes. When purge_after_days is set, purge is blocked for channels archived less than N days ago.
  • channel_retention config keys. Add to global_config: channel_retention.mode (flexible|retain, default flexible), channel_retention.purge_after_days (int|null, default null), channel_retention.archive_after_days (int|null, default null). Admin settings UI: "Retention" section under System.

8 · Surface Integration (deferred v0.22.8)

These were on the v0.22.8 checklist and haven't been wired yet.

  • ChatPane.primary bridge. ChatPane.create() in startApp() from server-rendered mount points. UI.renderMessages delegates to ChatPane.primary.renderMessages. Currently ChatPane.primary is defined but never assigned.
  • Theme save/load cycle. Wire Theme (from ui-primitives-additions.js) into settings appearance save/load. Persist preference to user settings API. CM6 theme sync on change.
  • Admin hybrid section loaders. Complete all JS-loaded admin sections (some still show empty panels).
  • Settings surface completeness. Models visibility toggles, User Personas CRUD, BYOK provider CRUD, Teams tab content.

Forward: Retention Policy Automation (post-0.23.2)

Not in scope for 0.23.2 but documents the planned direction.

Lifecycle: Active → Archived → Purged. Each transition is either user-initiated or policy-driven.

Auto-archive scanner. Background job (same pattern as compaction scanner). When channel_retention.archive_after_days is set, queries channels WHERE is_archived = false AND updated_at < NOW() - interval and sets is_archived = true. Runs on a configurable schedule (hourly default). Workflow channels auto-archive when their final stage completes — the scanner catches anything the workflow engine misses.

Auto-purge scanner. When channel_retention.purge_after_days is set, queries channels WHERE is_archived = true AND archived_at < NOW() - interval and hard-deletes in batches. This is also the floor for manual admin purge — admins cannot purge channels archived less than N days ago.

Audit trail. Every archive and purge action (manual or automatic) gets an audit log entry: action = "channel.archived" or action = "channel.purged" with actor (user ID or "system"), channel metadata snapshot, and timestamp.

Slots naturally into v0.24.x alongside RBAC (retention policy is inherently an admin/compliance concern) or as a standalone 0.23.x minor if it lands before auth work starts.