Changeset 0.23.2 (#155)
This commit is contained in:
399
docs/TODO-0.23.2.md
Normal file
399
docs/TODO-0.23.2.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# 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 3–4: 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.
|
||||
Reference in New Issue
Block a user