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

400 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- [x] Migration 016: `ai_mode`, `topic` on channels; `user_presence`
table; channel type constraint extended to `dm`, `channel`
- [x] `ai_mode` guard in `completion.go``off` returns 403,
`mention_only` without @mention returns `{status: delivered}`
- [x] `resolveMention()` resolves users (steps 34: exact + prefix
on `users.username`, self-mention blocked)
- [x] User @mention in completion handler — skips AI, attempts
`NotifyUserMention` via interface cast (hub method not yet
implemented — silently no-ops)
- [x] Presence endpoints: `POST /presence/heartbeat` (upsert),
`GET /presence?users=...` (90s threshold query)
- [x] Folders: full CRUD handler (`handlers/folders.go`), wired routes
- [x] `CreateChannel` accepts `type` field (default `direct`)
- [x] `ListChannels` supports `?types=dm,channel` multi-filter
- [x] Three-section sidebar in `chat.html` (Projects / Channels / Chats)
- [x] `renderChannelsSection()` in `ui-core.js` (# / person icons,
unread badges, online dots, active state)
- [x] `selectChannel()`, `newChannelOrDM()`, `loadChannels()` in
`projects-ui.js`
- [x] Folder UI: create, rename, delete, context menu, drag chats
- [x] Chat list filters out `type=channel` and `type=dm`
- [x] API methods: `createChannel(title,model,sp,type)`,
`listSidebarChannels()`, `presenceHeartbeat()`
- [x] `persona_groups` + `persona_group_members` tables (migration 004)
- [x] `PersonaGroup` / `PersonaGroupMember` model structs
- [x] Channel participants: CRUD handler, auto-created on channel create
---
## Bug Fixes
- [x] **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.channels``resp.data` in `projects-ui.js`.
- [x] **Folder drag-and-drop completely broken.** Three compounding
issues:
(a) `loadChats()` never mapped `c.folder``folderId` — 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.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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`.
- [x] **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).
- [x] **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`.
- [x] **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.avatar``u.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.
- [x] **`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.
- [x] **`selectChat()` migration.** Sets
`App.activeConversation = { id: chatId, type: chat.type }`.
Session restore reads/writes `cs-active-conversation` (JSON).
- [x] **`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.
- [x] **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.
- [x] **Model bar, input state, streaming.** All `currentChatId`
references → `App.activeId`. All `App.chats.find(...)` for
active chat → `App.getActiveChat()`. 12 files updated.
- [x] **Sidebar active highlight.** Both `renderChatList()` and
`renderChannelsSection()` check `App.activeId` for the
active class. Selecting either clears the other.
- [x] **URL sync.** `history.replaceState` uses
`/chat/${activeConversation.id}` regardless of type. The Go
route handler already loads any channel by ID.
- [x] **Session restore.** Reads `cs-active-conversation` JSON,
checks both `App.chats` and `App.channels` arrays, calls
`selectChat()` or `selectChannel()` as appropriate.
- [x] **`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
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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`.
- [x] **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).
- [x] **DM participant auto-creation.** `CreateChannel` adds all
`req.Participants` as member participants alongside the creator
(owner). Works for both Postgres and SQLite.
- [x] **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
- [x] **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`).
- [x] **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.
- [x] **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
- [x] **Client heartbeat.** 30s `setInterval` calling
`API.presenceHeartbeat()`. Start on app init, pause on
`visibilitychange` hidden, resume on visible.
- [x] **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.
- [x] **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
- [x] **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.
- [x] **"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.
- [x] **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.
- [x] **`@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).
- [x] **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
- [x] **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).
- [x] **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
- [x] **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).
- [x] **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.
- [x] **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.
- [x] **`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.
- [x] **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.
- [x] **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.
- [x] **Admin hybrid section loaders.** Complete all JS-loaded admin
sections (some still show empty panels).
- [x] **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.