Long-press message selection + delete (message.delete frame, outbox removal, all-device sync)
This commit is contained in:
1 parent
efecf2732e
commit
e9e1aed0f2
11 files changed
+417
-18
No files matched your search
@@ -63,7 +63,17 @@ Streaming a bubble. `update` carries the **full** current text (app replaces).
|
||||
{"type":"message.start","chat_id":"…","payload":{"message_id":"m_9002","role":"assistant"}}
|
||||
{"type":"message.update","chat_id":"…","payload":{"message_id":"m_9002","text":"partial…"}}
|
||||
{"type":"message.stop","chat_id":"…","payload":{"message_id":"m_9002","final_text":"full…",
|
||||
"reasoning":"…","model":"…","tokens":11}}
|
||||
"reasoning":"…","model":"…","tokens":11}}
|
||||
```
|
||||
|
||||
### `message.deleted`
|
||||
The given message(s) were deleted from a chat/thread. Response to a
|
||||
`message.delete` request (`id` set) **and** broadcast to every device so all of
|
||||
them drop the message(s) from their cache. Also outboxed, so a device that was
|
||||
offline learns of the deletion on its next `sync`.
|
||||
```json
|
||||
{"type":"message.deleted","id":30,"chat_id":"android:default","thread_id":null,
|
||||
"payload":{"message_ids":["m_9001","m_9002"]}}
|
||||
```
|
||||
|
||||
### `commentary`
|
||||
@@ -300,6 +310,16 @@ Load a page of messages for a chat/thread (initial open, scroll-up pagination).
|
||||
`before_message_id` — return messages older than this (omit for newest page).
|
||||
`limit` — max messages (default 50, max 200).
|
||||
|
||||
### `message.delete`
|
||||
Delete the given message(s) from a chat/thread. The server removes them from the
|
||||
outbox (so `history`/`sync` no longer return them) and broadcasts
|
||||
`message.deleted` to every device. Idempotent: a message already gone (pruned by
|
||||
retention) still yields a `message.deleted` broadcast so live caches drop it.
|
||||
```json
|
||||
{"type":"message.delete","id":30,"chat_id":"android:default","thread_id":null,
|
||||
"payload":{"message_ids":["m_9001","m_9002"]}}
|
||||
```
|
||||
|
||||
### `commands.catalog`
|
||||
Fetch the full slash-command catalog (for the `Menü` bottom sheet).
|
||||
```json
|
||||
@@ -346,10 +366,11 @@ Keepalive. `{"type":"ping","payload":{"ts":1724000000000}}` → `pong`.
|
||||
|
||||
- Frames are ordered per connection (TCP/WS). Streaming `message.update` frames
|
||||
for a `message_id` are monotonic; the app may coalesce to the latest.
|
||||
- Terminal frames (`message`, `message.stop`, `tool.end`, `notification`,
|
||||
`picker.*`, `channel.*`, `agent.busy`, `agent.idle`, `history`,
|
||||
`commands.catalog`, `commands.complete`) are **never dropped** under
|
||||
backpressure; only intermediate `message.update`/`tool.progress` are coalesced.
|
||||
- Terminal frames (`message`, `message.stop`, `message.deleted`, `tool.end`,
|
||||
`notification`, `picker.*`, `channel.*`, `agent.busy`, `agent.idle`,
|
||||
`history`, `commands.catalog`, `commands.complete`) are **never dropped**
|
||||
under backpressure; only intermediate `message.update`/`tool.progress` are
|
||||
coalesced.
|
||||
- Anything not delivered live goes to the **outbox** and is replayed by `sync`.
|
||||
- **Broadcast:** channel directory events (`channel.*`) and read-receipts are
|
||||
pushed to **all** connected devices for that gateway (no subscribe step).
|
||||
|
||||
@@ -126,6 +126,20 @@ app/shared/src/
|
||||
### 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
|
||||
(count + **Delete** + cancel ✕).
|
||||
- While selecting, **tap** a bubble to toggle it; the ✕ (or deselecting the last
|
||||
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.
|
||||
|
||||
### Agent busy / stop / steer
|
||||
- `agent.busy` → show "thinking…" indicator in chat header (animated dots).
|
||||
- `agent.idle` → clear indicator.
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"message.start": { "payload": { "message_id": { "type": "string" }, "role": { "type": "string" } } },
|
||||
"message.update": { "payload": { "message_id": { "type": "string" }, "text": { "type": "string", "description": "Full current text (app replaces)." } } },
|
||||
"message.stop": { "payload": { "message_id": { "type": "string" }, "final_text": { "type": "string" }, "reasoning": { "type": "string" }, "model": { "type": "string" }, "tokens": { "type": "integer" }, "ts": { "type": "integer" } } },
|
||||
"message.deleted": { "description": "The given message(s) were deleted from a chat/thread. Response to a message.delete request (id set) and broadcast to every device so all drop them from their cache; also outboxed so an offline device learns of the deletion on its next sync.", "payload": { "message_ids": { "type": "array", "items": { "type": "string" } } } },
|
||||
"commentary": { "description": "Intermediate assistant beat.", "payload": { "message_id": { "type": "string" }, "text": { "type": "string" } } },
|
||||
"tool.start": { "payload": { "index": { "type": "integer" }, "name": { "type": "string" }, "preview": { "type": "string" }, "args": { "type": "object" } } },
|
||||
"tool.progress": { "payload": { "index": { "type": "integer" }, "name": { "type": "string" }, "note": { "type": "string" } } },
|
||||
@@ -77,6 +78,7 @@
|
||||
"search": { "payload": { "query": { "type": "string" }, "scope": { "type": "string", "enum": ["all", "chat"] }, "chat_id": { "type": "string" }, "thread_id": { "type": "string" }, "limit": { "type": "integer", "description": "Optional; server default 20." } } },
|
||||
"sync": { "description": "Reconnect catch-up; replays undelivered outbox frames only (not full history).", "payload": { "cursor": { "type": "integer" } } },
|
||||
"history": { "description": "Load a page of full message history for a chat/thread (initial open, scroll-up pagination).", "payload": { "before_message_id": { "type": "string", "description": "Return messages older than this (omit for newest page)." }, "limit": { "type": "integer", "description": "Max messages (default 50, max 200)." } } },
|
||||
"message.delete": { "description": "Delete the given message(s) from a chat/thread. The server removes them from the outbox (so history/sync no longer return them) and broadcasts message.deleted to every device. Idempotent: a message already gone (pruned) still yields a message.deleted broadcast.", "payload": { "message_ids": { "type": "array", "items": { "type": "string" }, "description": "One or more message_id values to delete." } } },
|
||||
"fcm.register": { "payload": { "fcm_token": { "type": "string" }, "ntfy_topic": { "type": "string" } } },
|
||||
"ping": { "payload": { "ts": { "type": "integer" } } }
|
||||
}
|
||||
@@ -105,7 +107,7 @@
|
||||
],
|
||||
"reliability": {
|
||||
"ordering": "Per-connection (TCP/WS). message.update for a message_id is monotonic; app may coalesce to latest.",
|
||||
"never_dropped": ["message", "message.stop", "tool.end", "notification", "channel.*", "search.results", "error"],
|
||||
"never_dropped": ["message", "message.stop", "message.deleted", "tool.end", "notification", "channel.*", "search.results", "error"],
|
||||
"coalescable_under_backpressure": ["message.update", "tool.progress"],
|
||||
"offline": "Undelivered frames go to the outbox; replayed by sync. Terminal frames always outboxed."
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user