Summary
The #general thread in native chat cannot be deleted — three layers of guards prevent it (store, handler, UI). Users who want to reorganize their threads around project-specific topics are stuck with the default #general thread forever. The request is to make #general deletable while enforcing that at least one thread always exists, and falling back to the first thread in the list when no #general is present.
Current state
#General deletion guards (three layers)
- Store layer —
DeleteTopic in both SQLite (webchannel_store.go:874-884) and Postgres (webchannel_store_postgres.go:505-514) checks is_general and returns "cannot delete #general topic".
- Handler layer —
handleTopicDelete (handlers_chat_v2.go:664-667) surfaces the error as HTTP 400.
- UI layer —
chat-space-rail.ts:1663 hides Delete/Rename menu items for isGeneral threads; handleDeleteThread (line 1245) has an early return guard.
Navigation assumes #general exists
selectSpaceBySlug (chat.ts:1282-1301) uses threads.find(t => t.isGeneral) with no fallback to threads[0].
handleCollapsedSpaceClick (chat-space-rail.ts:929-933) — same pattern, no fallback.
Lazy #general re-creation (critical interaction)
ListTopics in both store implementations auto-creates #general if none exists (webchannel_store_postgres.go:453-464, webchannel_store.go:828-843). If #general becomes deletable, this lazy creation would immediately re-create it, defeating the purpose.
No "last thread" guard
Neither store nor handler checks thread count before deletion.
What must change
1. Make #general deletable
- Store: Remove
is_general guard in DeleteTopic (both dialects). Replace with a "last remaining thread" count check.
- Handler: Replace the
#general error with a "cannot delete the last thread" error.
- UI: Remove
!thread.isGeneral guards hiding Delete from the context menu. Show error to user when "last thread" deletion is rejected.
2. Enforce at-least-one-thread invariant
- Store: Before soft-deleting, count active (non-deleted) topics for the project. If count ≤ 1, return error.
- Handler: Surface as 400 validation error.
3. Navigation fallback when no #general
selectSpaceBySlug (chat.ts:1282-1301): Fallback to threads[0] when no isGeneral thread found.
handleCollapsedSpaceClick (chat-space-rail.ts:929-933): Same fallback.
- No sort changes needed — existing sort already produces stable order.
4. Remove lazy #general re-creation
- Remove auto-create from
ListTopics (webchannel_store_postgres.go:453-464, webchannel_store.go:828-843). Keep EnsureGeneralTopic only on project creation path (handlers_projects_core.go:592-625).
5. Decide on #general rename
- Currently blocked (
handleTopicPatch:571-574). If #general is deletable, renaming should also be allowed. Remove the rename guard.
Edge cases
- Messages in deleted threads: No FK constraints — messages survive soft-delete but become inaccessible via UI (history endpoint returns 404 for deleted topics). Acceptable for v1.
- Read state orphaning:
webchat_read_state rows keyed by topic UUID become orphaned. Harmless but accumulate.
- Conversation entity: Topic deletion does not delete the linked
conversations row. May need cleanup.
Sizing
Small — changes are well-localized: 2 store files, 1 handler file, 2 frontend files, plus test updates. Conditional pivot to Medium if rename support, data migration, or conversation-entity cleanup are included.
Files
pkg/hub/webchannel_store_postgres.go:453-464,503-523,546-595 — Postgres store (lazy creation, delete guard, ensure)
pkg/hub/webchannel_store.go:828-843,872-893 — SQLite store (lazy creation, delete guard)
pkg/hub/handlers_chat_v2.go:524-676 — Handler (patch/delete guards)
web/src/components/shared/chat/chat-space-rail.ts:1220,1245,1663 — UI guards
web/src/components/pages/chat.ts:1260-1303 — Navigation fallback
- Investigation doc:
scratchpad/projects/roadmap/inv-chat-thread-deletion.md
Summary
The #general thread in native chat cannot be deleted — three layers of guards prevent it (store, handler, UI). Users who want to reorganize their threads around project-specific topics are stuck with the default #general thread forever. The request is to make #general deletable while enforcing that at least one thread always exists, and falling back to the first thread in the list when no #general is present.
Current state
#General deletion guards (three layers)
DeleteTopicin both SQLite (webchannel_store.go:874-884) and Postgres (webchannel_store_postgres.go:505-514) checksis_generaland returns"cannot delete #general topic".handleTopicDelete(handlers_chat_v2.go:664-667) surfaces the error as HTTP 400.chat-space-rail.ts:1663hides Delete/Rename menu items forisGeneralthreads;handleDeleteThread(line 1245) has an early return guard.Navigation assumes #general exists
selectSpaceBySlug(chat.ts:1282-1301) usesthreads.find(t => t.isGeneral)with no fallback tothreads[0].handleCollapsedSpaceClick(chat-space-rail.ts:929-933) — same pattern, no fallback.Lazy #general re-creation (critical interaction)
ListTopicsin both store implementations auto-creates #general if none exists (webchannel_store_postgres.go:453-464,webchannel_store.go:828-843). If #general becomes deletable, this lazy creation would immediately re-create it, defeating the purpose.No "last thread" guard
Neither store nor handler checks thread count before deletion.
What must change
1. Make #general deletable
is_generalguard inDeleteTopic(both dialects). Replace with a "last remaining thread" count check.#generalerror with a"cannot delete the last thread"error.!thread.isGeneralguards hiding Delete from the context menu. Show error to user when "last thread" deletion is rejected.2. Enforce at-least-one-thread invariant
3. Navigation fallback when no #general
selectSpaceBySlug(chat.ts:1282-1301): Fallback tothreads[0]when noisGeneralthread found.handleCollapsedSpaceClick(chat-space-rail.ts:929-933): Same fallback.4. Remove lazy #general re-creation
ListTopics(webchannel_store_postgres.go:453-464,webchannel_store.go:828-843). KeepEnsureGeneralTopiconly on project creation path (handlers_projects_core.go:592-625).5. Decide on #general rename
handleTopicPatch:571-574). If #general is deletable, renaming should also be allowed. Remove the rename guard.Edge cases
webchat_read_staterows keyed by topic UUID become orphaned. Harmless but accumulate.conversationsrow. May need cleanup.Sizing
Small — changes are well-localized: 2 store files, 1 handler file, 2 frontend files, plus test updates. Conditional pivot to Medium if rename support, data migration, or conversation-entity cleanup are included.
Files
pkg/hub/webchannel_store_postgres.go:453-464,503-523,546-595— Postgres store (lazy creation, delete guard, ensure)pkg/hub/webchannel_store.go:828-843,872-893— SQLite store (lazy creation, delete guard)pkg/hub/handlers_chat_v2.go:524-676— Handler (patch/delete guards)web/src/components/shared/chat/chat-space-rail.ts:1220,1245,1663— UI guardsweb/src/components/pages/chat.ts:1260-1303— Navigation fallbackscratchpad/projects/roadmap/inv-chat-thread-deletion.md