Make thread/channel/message deletion complete (hard delete)

Deleting a thread, channel, or message was a no-op/soft-delete: messages
were only dropped from the plugin outbox (still in hermes' session store,
hence searchable/recoverable) and channels/threads were merely archived.

Now deletion is complete and non-recoverable, with no search trace:

- purge.py (new): hard-delete from hermes' session store (state.db).
  delete_lane wipes a channel's/thread's sessions + messages; deleting a
  messages row also drops it from the FTS5 index via the delete triggers.
  delete_message removes one message, matched by (session, role, exact
  content, closest timestamp) since plugin m_<hex> ids aren't persisted.
- channels.py: delete() hard-deletes the row (and a channel's child
  threads) instead of archiving.
- outbox.py: add delete_lane() (wipe all frames for a lane) and
  message_info() (read a message's final role/text/ts for the match).
- adapter.py: on_channel_delete wipes outbox + session store;
  on_message_delete purges the session-store row per message.
- App: delete confirmations no longer claim history stays for search;
  ChannelStore removes a channel's threads on channel delete.
- Docs updated to describe hard deletion.
This commit is contained in:
ARIA committed 2026-08-21 19:47:55 +02:00
1 parent 9286937e2d
commit 17bf41a0b9
11 files changed
+373 -51

No files matched your search

+29 -10
View File
@@ -6,7 +6,7 @@ the code is in `app/shared` (commonMain) so the Desktop app reuses it.
## 10.1 Tech stack
| Concern | Choice |
|---|---|
| --- | --- |
| Language | Kotlin |
| UI | Jetpack Compose (Material 3), Compose Navigation |
| Async | Kotlinx Coroutines + Flow |
@@ -74,6 +74,7 @@ app/shared/src/
## 10.5 Feature implementation (your checklist)
### Input box, auto-grow (max height)
- Compose `BasicTextField` inside a `Box` with
`Modifier.heightIn(min = 1.line, max = 160.dp)`. Grows with lines, caps at
160dp, then scrolls internally.
@@ -82,6 +83,7 @@ app/shared/src/
(configurable: Enter=send vs Enter=newline).
### Slash commands
- **`/` drawer (implemented):** typing `/` in the composer rolls a drawer up
over the input listing the command catalog. The catalog is served by the
gateway via `commands.catalog` request → response with
@@ -102,6 +104,7 @@ app/shared/src/
sheet with the options; answer via `picker.select`) — planned.
### Streaming (app-controlled)
- **Settings → "Streaming"** toggle (default on). When off, the app ignores
`message.start`/`message.update` frames and shows each reply as a single
final message on `message.stop` (the typing indicator covers the wait).
@@ -109,6 +112,7 @@ app/shared/src/
devices (see `05-streaming.md` §5.1).
### Tool output (app-controlled verbosity)
- `ToolCard` renders `tool.start/progress/end` frames.
- The gateway always supplies the **full** tool data: it forces
`display.platforms.android.tool_progress: verbose` (so the progress line
@@ -123,6 +127,7 @@ app/shared/src/
- Spinner while running; ✓/✗ + duration on `tool.end`.
### Reasoning before message
- `ReasoningBlock` (collapsible, "💭 Reasoning" header, monospace body, **copy**
button) rendered **above** the message body from the `reasoning` field.
Matches the reference screenshot.
@@ -132,9 +137,11 @@ app/shared/src/
tap-to-toggle always works.
### Intermediate messages
- `commentary` frames → dimmed/smaller bubble, distinct from final answers.
### Message selection + delete
- **Long-press** a message bubble (touch) or **right-click** it (desktop mouse)
enters selection mode: the tapped message is selected (a circular check appears
beside each bubble) and the composer is replaced by a selection toolbar
@@ -143,12 +150,15 @@ app/shared/src/
message) exits selection mode. Only finalized messages are selectable — a
streaming bubble has no final id yet and a pending echo isn't on the server.
- **Delete** → confirm dialog → `message.delete {message_ids:[…]}` for the
current lane. The server removes the message(s) from the outbox (so
`history`/`sync` no longer return them) and broadcasts `message.deleted` to
every device; each device drops them from its cache (the requesting device
also drops them locally for snappy UX). Deleting is idempotent.
current lane. The server completely deletes the message(s): they are removed
from the outbox (so `history`/`sync` no longer return them) **and** from the
hermes session store (so no search trace survives and they are not
recoverable), and `message.deleted` is broadcast to every device; each device
drops them from its cache (the requesting device also drops them locally for
snappy UX). Deleting is idempotent.
### Agent busy / stop / steer
- `agent.busy` → show "thinking…" indicator in chat header (animated dots).
- `agent.idle` → clear indicator.
- **Stop button** (appears in header while busy): sends `agent.stop`.
@@ -156,8 +166,9 @@ app/shared/src/
`agent.steer` (injects mid-turn) rather than queuing a new `message.send`.
### Threading + channels
- **Channel list** (drawer on single-pane; left rail on two-pane) = default chat
+ user channels (avatar, name, last-message preview, unread badge, active
- user channels (avatar, name, last-message preview, unread badge, active
highlight bar) — matches the reference left sidebar.
- **Thread toggle** in the default chat header: "Threads on/off". On → topic
switcher above the message list (each topic = a `thread_id`).
@@ -171,23 +182,27 @@ app/shared/src/
menu offers "Set as cron target".
- **Topic context menu** — long-press a topic chip (touch) or right-click it
(desktop mouse) → "Rename" / "Delete". Rename → `channel.rename` (prefilled
dialog); Delete → confirm → `channel.delete` (soft-delete; the thread leaves
the switcher and, if it was the open lane, the app falls back to the channel's
flat lane). The right-click handler is a skiko `expect`/`actual`
dialog); Delete → confirm → `channel.delete` (hard delete; the thread leaves
the switcher, its history is wiped from the outbox and session store, and if
it was the open lane the app falls back to the channel's flat lane). The
right-click handler is a skiko `expect`/`actual`
(`iris/ui/ContextMenu.kt`); on touch it is a no-op (long-press covers it).
### Search
- Search bar (chat header or top) with a **scope toggle**: "Search everywhere" /
"Search in this chat/channel". → `search` frame → results list → tap jumps to
the message (navigate + highlight).
### Attach media
- Paperclip → system pickers (Photos / Files / Audio / Video / Docs) via SAF.
- Selected files show as **preview chips** in the composer (thumbnail + name +
remove). On send: `media.upload` (chunked) for each, then `message.send` with
`media_refs`.
### Voice input (mic button)
- Mic button (right of composer, toggles to send when text is present).
- Tap → request `RECORD_AUDIO` permission → start recording (MediaRecorder,
OGG/Opus, 44.1 kHz mono).
@@ -199,11 +214,13 @@ app/shared/src/
transcribes it. No client-side STT.
### Push notifications
- FCM service (see `08-push.md`): foreground banner + background foreground
service → `sync`. Notification channel per chat. Tap → deep-link to chat.
- ntfy fallback: foreground service maintains the subscription.
### Live playback
- AI-sent audio/video → `media.pull` → cache file → **ExoPlayer** inline player
(audio: mini-player; video: inline + fullscreen + PiP). Documents/images →
viewer / open-with.
@@ -211,12 +228,14 @@ app/shared/src/
## 10.6 Layout (Telegram-style, per reference image)
**Two layout modes** (decision: user-toggleable, **single-pane default**):
- **Single-pane (default on phones):** chat full-screen; channel list in a
swipeable drawer (hamburger / edge swipe).
- **Two-pane (Telegram-style, like the reference):** persistent left channel
rail + chat. Auto-enabled on tablets / large screens; toggleable in Settings.
**Chat screen anatomy (matches reference):**
- **Header:** back (single-pane), avatar, name + "Bot" subtitle, edit + overflow
(⋮) menu (thread toggle, channel menu, set cron target, clear).
- **Message list:** date separators ("7. August"); user bubbles **right**
@@ -245,4 +264,4 @@ color. Accent = user's chosen brand color (default indigo, like the reference).
does a real `hello` (not just a TCP probe — per hermes desktop guidance, the
auth leg must be exercised). On success → save (secure storage) → main.
- States: connecting / connected / reconnecting / degraded / auth-failed — each
with honest copy and a way out.
with honest copy and a way out.