|
@@ -1,17 +1,17 @@
|
|
|
---
|
|
---
|
|
|
type: architecture
|
|
type: architecture
|
|
|
title: Application state and event handling
|
|
title: Application state and event handling
|
|
|
-description: How the App struct in src/app.rs owns the application state and routes every AppEvent (keys, WhatsApp events, AI replies, image descriptions, audio transcriptions, reply timers, web panel commands, configuration reloads, relink outcomes), including the image and audio bookkeeping maps and sets, function-key handling, the reply orchestration order and the background tasks.
|
|
|
|
|
-tags: [app, state, events, keybindings, tokio, web-panel, relink, config-reload, transcription]
|
|
|
|
|
|
|
+description: How the App struct in src/app.rs owns the application state and routes every AppEvent (keys, WhatsApp events, AI replies, image descriptions, audio transcriptions, reply timers, web panel commands, configuration reloads, relink outcomes, avatar fetches), including the image and audio bookkeeping, the web panel avatar cache, function-key handling, the reply orchestration order and the background tasks.
|
|
|
|
|
+tags: [app, state, events, keybindings, tokio, web-panel, relink, config-reload, transcription, avatars]
|
|
|
verified:
|
|
verified:
|
|
|
- by: openwiki/0.7.2
|
|
- by: openwiki/0.7.2
|
|
|
- at: 2026-10-24T16:42:08.550Z
|
|
|
|
|
|
|
+ at: 2026-10-25T12:54:53.684Z
|
|
|
sources:
|
|
sources:
|
|
|
- id: openwiki-source-9fec9d1af408501c9c455161
|
|
- id: openwiki-source-9fec9d1af408501c9c455161
|
|
|
resource: repo://src/app.rs
|
|
resource: repo://src/app.rs
|
|
|
- id: openwiki-source-ce0c4ea8cd3b15431fa691ce
|
|
- id: openwiki-source-ce0c4ea8cd3b15431fa691ce
|
|
|
resource: repo://src/history.rs
|
|
resource: repo://src/history.rs
|
|
|
-generated: { by: "openwiki/0.7.2", at: "2026-10-24T16:42:08.550Z" }
|
|
|
|
|
|
|
+generated: { by: "claude-code", at: "2026-10-25T12:54:53.684Z" }
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
# Application state and event handling
|
|
# Application state and event handling
|
|
@@ -25,7 +25,7 @@ and [web panel](../integrations/web-control-panel.md) activity.
|
|
|
|
|
|
|
|
## Events
|
|
## Events
|
|
|
|
|
|
|
|
-`AppEvent` (`src/app.rs:117-161`) is the single message type of the main
|
|
|
|
|
|
|
+`AppEvent` (`src/app.rs:125-171`) is the single message type of the main
|
|
|
channel:
|
|
channel:
|
|
|
|
|
|
|
|
| Variant | Producer | Meaning |
|
|
| Variant | Producer | Meaning |
|
|
@@ -34,15 +34,17 @@ channel:
|
|
|
| `Redraw` | input thread | Terminal resized; nothing to change, just redraw. |
|
|
| `Redraw` | input thread | Terminal resized; nothing to change, just redraw. |
|
|
|
| `Wa(WaEvent)` | WhatsApp forwarder | An event from the [WhatsApp client](../integrations/whatsapp-client.md). |
|
|
| `Wa(WaEvent)` | WhatsApp forwarder | An event from the [WhatsApp client](../integrations/whatsapp-client.md). |
|
|
|
| `AiReply { chat_jid, result }` | AI background task | `Reply::Text`, `Reply::NoReply` (the model called the `no_reply` tool) or an error string. |
|
|
| `AiReply { chat_jid, result }` | AI background task | `Reply::Text`, `Reply::NoReply` (the model called the `no_reply` tool) or an error string. |
|
|
|
-| `ImagesDescribed { target, results }` | image description collector | One description (or error) per image, keyed by message id; `target` is a `DescriptionTarget` (`src/app.rs:163-169`) holding the chat and whether to answer it afterwards. |
|
|
|
|
|
|
|
+| `ImagesDescribed { target, results }` | image description collector | One description (or error) per image, keyed by message id; `target` is a `DescriptionTarget` (`src/app.rs:182-188`) holding the chat and whether to answer it afterwards. |
|
|
|
| `AudiosTranscribed { chat_jid, then_reply, results, images }` | audio transcription collector | One transcription (or error) per audio, keyed by message id; `images` are the images postponed until the transcriptions are stored and `then_reply` whether to answer the chat afterwards. |
|
|
| `AudiosTranscribed { chat_jid, then_reply, results, images }` | audio transcription collector | One transcription (or error) per audio, keyed by message id; `images` are the images postponed until the transcriptions are stored and `then_reply` whether to answer the chat afterwards. |
|
|
|
| `ReplyDue { chat_jid }` | reply timer task | The reply delay of a chat has elapsed. |
|
|
| `ReplyDue { chat_jid }` | reply timer task | The reply delay of a chat has elapsed. |
|
|
|
| `Sent { chat_jid, text, is_ai, result }` | send background task | Outcome of a text send: `Ok` carries the new message id. |
|
|
| `Sent { chat_jid, text, is_ai, result }` | send background task | Outcome of a text send: `Ok` carries the new message id. |
|
|
|
| `Web(WebRequest)` | `wh-web` thread | A command from the [web control panel](../integrations/web-control-panel.md) with a oneshot reply channel. |
|
|
| `Web(WebRequest)` | `wh-web` thread | A command from the [web control panel](../integrations/web-control-panel.md) with a oneshot reply channel. |
|
|
|
| `ConfigReloaded(Result<Box<Config>, String>)` | configuration watcher task | A configuration file changed: the reloaded `Config` or the load error. |
|
|
| `ConfigReloaded(Result<Box<Config>, String>)` | configuration watcher task | A configuration file changed: the reloaded `Config` or the load error. |
|
|
|
| `Relinked(Result<(), String>)` | relink task | Outcome of a relink: the new unpaired session started, or the error. |
|
|
| `Relinked(Result<(), String>)` | relink task | Outcome of a relink: the new unpaired session started, or the error. |
|
|
|
|
|
+| `AvatarFetched { jid, url }` | avatar fetch task | The profile picture URL of one chat; `None` when it has no picture, hides it or the fetch failed. |
|
|
|
|
|
+| `AvatarFetchFinished { error }` | avatar fetch task | The avatar batch ended: `None` when complete, otherwise the error that interrupted it. |
|
|
|
|
|
|
|
|
-`handle_event` (`src/app.rs:319-351`) dispatches each variant; after an
|
|
|
|
|
|
|
+`handle_event` (`src/app.rs:352-390`) dispatches each variant; after an
|
|
|
`AiReply` it always calls `finish_reply` to end the chat's pending state, and
|
|
`AiReply` it always calls `finish_reply` to end the chat's pending state, and
|
|
|
`AudiosTranscribed` is routed to `handle_audios_transcribed`. Only fatal
|
|
`AudiosTranscribed` is routed to `handle_audios_transcribed`. Only fatal
|
|
|
history (SQLite) errors propagate as `Err`; every other failure is turned into
|
|
history (SQLite) errors propagate as `Err`; every other failure is turned into
|
|
@@ -50,13 +52,13 @@ a `status` text shown in the footer.
|
|
|
|
|
|
|
|
## State
|
|
## State
|
|
|
|
|
|
|
|
-`App` (`src/app.rs:208-267`) exposes UI fields read by the renderer: `screen`,
|
|
|
|
|
|
|
+`App` (`src/app.rs:228-292`) exposes UI fields read by the renderer: `screen`,
|
|
|
`qr_code`, `focus`, `list_mode`, `chats`, `contacts`, `selected`, `filter`,
|
|
`qr_code`, `focus`, `list_mode`, `chats`, `contacts`, `selected`, `filter`,
|
|
|
`open_chat`, `open_chat_name`, `open_chat_autoreply`, `global_autoreply`,
|
|
`open_chat`, `open_chat_name`, `open_chat_autoreply`, `global_autoreply`,
|
|
|
`messages`, `input`, `status` and `should_quit`. Private fields hold the
|
|
`messages`, `input`, `status` and `should_quit`. Private fields hold the
|
|
|
`History`, the `Config`, an optional `AiClient`, the autoreply `ReplyGate`
|
|
`History`, the `Config`, an optional `AiClient`, the autoreply `ReplyGate`
|
|
|
(defined in `src/autoreply.rs`), the WhatsApp handle and a clone of the event
|
|
(defined in `src/autoreply.rs`), the WhatsApp handle and a clone of the event
|
|
|
-sender, plus the reply bookkeeping:
|
|
|
|
|
|
|
+sender, plus the reply and web panel bookkeeping:
|
|
|
|
|
|
|
|
| Field | Purpose |
|
|
| Field | Purpose |
|
|
|
|---|---|
|
|
|---|---|
|
|
@@ -71,20 +73,24 @@ sender, plus the reply bookkeeping:
|
|
|
| `pending_delete` | the chat awaiting a deletion confirmation |
|
|
| `pending_delete` | the chat awaiting a deletion confirmation |
|
|
|
| `pending_relink` | a first F8 press awaits the second one that confirms the relink |
|
|
| `pending_relink` | a first F8 press awaits the second one that confirms the relink |
|
|
|
| `relinking` | a relink is running; further relink requests are ignored until it ends |
|
|
| `relinking` | a relink is running; further relink requests are ignored until it ends |
|
|
|
|
|
+| `avatars` | in-memory cache of web panel profile pictures by chat JID (`CachedAvatar`: URL or `None`, plus fetch instant) |
|
|
|
|
|
+| `avatar_fetch_running` | an avatar fetch task is running (at most one at a time) |
|
|
|
|
|
+| `wa_connected` | the WhatsApp client is currently connected; gates the avatar fetch |
|
|
|
|
|
|
|
|
-- `Screen` (`src/app.rs:171-177`): `Connecting` → `Pairing` (QR shown) → `Main`.
|
|
|
|
|
-- `Focus` (`src/app.rs:179-184`): `List` or `Input`.
|
|
|
|
|
-- `ListMode` (`src/app.rs:186-191`): `Chats` or `Contacts`.
|
|
|
|
|
|
|
+- `Screen` (`src/app.rs:191-197`): `Connecting` → `Pairing` (QR shown) → `Main`.
|
|
|
|
|
+- `Focus` (`src/app.rs:199-204`): `List` or `Input`.
|
|
|
|
|
+- `ListMode` (`src/app.rs:206-211`): `Chats` or `Contacts`.
|
|
|
|
|
|
|
|
-`App::new` (`src/app.rs:269-317`) takes an `AppDeps` (`src/app.rs:193-199`) —
|
|
|
|
|
|
|
+`App::new` (`src/app.rs:299-345`) takes an `AppDeps` (`src/app.rs:213-219`) —
|
|
|
the configuration, history, WhatsApp handle and event sender — builds the AI
|
|
the configuration, history, WhatsApp handle and event sender — builds the AI
|
|
|
client with `build_ai`, loads chats, contacts and the persisted global
|
|
client with `build_ai`, loads chats, contacts and the persisted global
|
|
|
autoreply flag from history, starts on `Connecting` with list focus in chat
|
|
autoreply flag from history, starts on `Connecting` with list focus in chat
|
|
|
-mode, an empty filter, no open chat, no pending deletion or relink and empty
|
|
|
|
|
-image/audio bookkeeping, and creates the `ReplyGate` with the current time, so
|
|
|
|
|
|
|
+mode, an empty filter, no open chat, no pending deletion or relink, empty
|
|
|
|
|
+image/audio bookkeeping, an empty avatar cache with no fetch running and
|
|
|
|
|
+`wa_connected` false, and creates the `ReplyGate` with the current time, so
|
|
|
that backlog messages are never auto-answered live.
|
|
that backlog messages are never auto-answered live.
|
|
|
|
|
|
|
|
-`build_ai` (`src/app.rs:1874-1887`) returns no client when the system prompt
|
|
|
|
|
|
|
+`build_ai` (`src/app.rs:1953-1966`) returns no client when the system prompt
|
|
|
(`SOUL.md`, see [Configuration](../operations/configuration-and-data.md)) is
|
|
(`SOUL.md`, see [Configuration](../operations/configuration-and-data.md)) is
|
|
|
missing — status `AI non configurata: imposta openai_api_key in config.toml e
|
|
missing — status `AI non configurata: imposta openai_api_key in config.toml e
|
|
|
il prompt in SOUL.md` — and reports a client build error with the
|
|
il prompt in SOUL.md` — and reports a client build error with the
|
|
@@ -94,7 +100,7 @@ il prompt in SOUL.md` — and reports a client build error with the
|
|
|
|
|
|
|
|
The [configuration watcher](../operations/configuration-and-data.md#live-reload)
|
|
The [configuration watcher](../operations/configuration-and-data.md#live-reload)
|
|
|
sends `ConfigReloaded` when `config.toml`, `SOUL.md` or `VISION.md` change.
|
|
sends `ConfigReloaded` when `config.toml`, `SOUL.md` or `VISION.md` change.
|
|
|
-`apply_reloaded_config` (`src/app.rs:417-434`) keeps the current configuration
|
|
|
|
|
|
|
+`apply_reloaded_config` (`src/app.rs:462-474`) keeps the current configuration
|
|
|
and shows `Configurazione non ricaricata: <error>` when the load failed;
|
|
and shows `Configurazione non ricaricata: <error>` when the load failed;
|
|
|
otherwise it replaces `config`, rebuilds the AI client with `build_ai` (replies
|
|
otherwise it replaces `config`, rebuilds the AI client with `build_ai` (replies
|
|
|
already running keep the client they were started with) and shows
|
|
already running keep the client they were started with) and shows
|
|
@@ -104,17 +110,20 @@ delays, retries and `transcribe_*` settings without a restart.
|
|
|
|
|
|
|
|
## WhatsApp events
|
|
## WhatsApp events
|
|
|
|
|
|
|
|
-`handle_wa_event` (`src/app.rs:436-463`):
|
|
|
|
|
|
|
+`handle_wa_event` (`src/app.rs:480-509`):
|
|
|
|
|
|
|
|
- `Qr` → switch to `Pairing` and store the QR payload.
|
|
- `Qr` → switch to `Pairing` and store the QR payload.
|
|
|
-- `Connected` → switch to `Main`, clear the QR, status `Connesso`, refresh own JIDs.
|
|
|
|
|
-- `Disconnected` / `Error` → copy the text into `status` only.
|
|
|
|
|
-- `LoggedOut` → back to `Connecting`, clear the QR, status `Sessione scollegata`.
|
|
|
|
|
|
|
+- `Connected` → set `wa_connected`, switch to `Main`, clear the QR, status
|
|
|
|
|
+ `Connesso`, refresh own JIDs.
|
|
|
|
|
+- `Disconnected` → clear `wa_connected` and copy the text into `status`.
|
|
|
|
|
+- `Error` → copy the text into `status` only (the connection flag is kept).
|
|
|
|
|
+- `LoggedOut` → clear `wa_connected`, back to `Connecting`, clear the QR,
|
|
|
|
|
+ status `Sessione scollegata`.
|
|
|
- `OfflineSyncCompleted` → `on_offline_sync_completed`, the startup catch-up
|
|
- `OfflineSyncCompleted` → `on_offline_sync_completed`, the startup catch-up
|
|
|
(see below).
|
|
(see below).
|
|
|
- `Contact` → `on_contact`: upsert the contact, reload contacts and chats and
|
|
- `Contact` → `on_contact`: upsert the contact, reload contacts and chats and
|
|
|
- refresh the open chat name (`src/app.rs:465-477`).
|
|
|
|
|
-- `Message { message, image, audio }` → `on_message` (`src/app.rs:479-499`).
|
|
|
|
|
|
|
+ refresh the open chat name (`src/app.rs:515-523`).
|
|
|
|
|
+- `Message { message, image, audio }` → `on_message` (`src/app.rs:531-550`).
|
|
|
|
|
|
|
|
`on_message` saves the message and stops immediately when
|
|
`on_message` saves the message and stops immediately when
|
|
|
`History::save_message` reports it was not new, so history sync replays never
|
|
`History::save_message` reports it was not new, so history sync replays never
|
|
@@ -123,7 +132,7 @@ open conversation and then go through `maybe_autoreply`, which keeps the
|
|
|
message's image and/or audio when the reply is allowed (see
|
|
message's image and/or audio when the reply is allowed (see
|
|
|
[AI autoreply](../workflows/ai-autoreply.md)).
|
|
[AI autoreply](../workflows/ai-autoreply.md)).
|
|
|
|
|
|
|
|
-Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:1030-1042`), which
|
|
|
|
|
|
|
+Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:1084-1096`), which
|
|
|
replaces the gate's JIDs only when `WaHandle::own_jids` reports a non-empty set,
|
|
replaces the gate's JIDs only when `WaHandle::own_jids` reports a non-empty set,
|
|
|
so `own_jids_known` stays false and the refresh is retried later while the
|
|
so `own_jids_known` stays false and the refresh is retried later while the
|
|
|
device is not paired (or no session is running during a relink). It is called
|
|
device is not paired (or no session is running during a relink). It is called
|
|
@@ -135,19 +144,19 @@ F8 (twice) and the web panel's `Relink` command replace the WhatsApp session
|
|
|
with a new unpaired one; the full flow is in
|
|
with a new unpaired one; the full flow is in
|
|
|
[WhatsApp relink and pairing](../workflows/relink-and-pairing.md). In `App`:
|
|
[WhatsApp relink and pairing](../workflows/relink-and-pairing.md). In `App`:
|
|
|
|
|
|
|
|
-- `request_relink` (`src/app.rs:390-403`) cancels a pending chat deletion; the
|
|
|
|
|
|
|
+- `request_relink` (`src/app.rs:435-443`) cancels a pending chat deletion; the
|
|
|
first F8 sets `pending_relink` and shows
|
|
first F8 sets `pending_relink` and shows
|
|
|
`Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla`,
|
|
`Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla`,
|
|
|
the second calls `start_relink`.
|
|
the second calls `start_relink`.
|
|
|
-- `cancel_pending_relink` (`src/app.rs:405-415`) clears the flag on any other
|
|
|
|
|
|
|
+- `cancel_pending_relink` (`src/app.rs:449-455`) clears the flag on any other
|
|
|
key and shows `Nuovo abbinamento annullato`.
|
|
key and shows `Nuovo abbinamento annullato`.
|
|
|
-- `start_relink` (`src/app.rs:353-375`) is ignored while a relink runs;
|
|
|
|
|
- otherwise it switches to `Connecting`, clears the QR, shows
|
|
|
|
|
- `Cancellazione della sessione in corso...`, resets `own_jids_known` (the new
|
|
|
|
|
- pairing may be another account) and spawns a task that runs
|
|
|
|
|
|
|
+- `start_relink` (`src/app.rs:399-415`) is ignored while a relink runs;
|
|
|
|
|
+ otherwise it clears `wa_connected`, switches to `Connecting`, clears the QR,
|
|
|
|
|
+ shows `Cancellazione della sessione in corso...`, resets `own_jids_known`
|
|
|
|
|
+ (the new pairing may be another account) and spawns a task that runs
|
|
|
`WaHandle::relink` and sends `Relinked`. The web command calls it directly,
|
|
`WaHandle::relink` and sends `Relinked`. The web command calls it directly,
|
|
|
without the two-press confirmation (the browser asks instead).
|
|
without the two-press confirmation (the browser asks instead).
|
|
|
-- `finish_relink` (`src/app.rs:377-388`) clears `relinking` and shows
|
|
|
|
|
|
|
+- `finish_relink` (`src/app.rs:422-428`) clears `relinking` and shows
|
|
|
`Sessione cancellata: scansiona il nuovo QR` or
|
|
`Sessione cancellata: scansiona il nuovo QR` or
|
|
|
`Nuovo abbinamento fallito: <error>`; the screen then follows the new
|
|
`Nuovo abbinamento fallito: <error>`; the screen then follows the new
|
|
|
session's `Qr` / `Connected` events.
|
|
session's `Qr` / `Connected` events.
|
|
@@ -155,8 +164,8 @@ with a new unpaired one; the full flow is in
|
|
|
## Autoreply orchestration
|
|
## Autoreply orchestration
|
|
|
|
|
|
|
|
A chat is "enabled" when the global autoreply is on **or** its per-chat flag
|
|
A chat is "enabled" when the global autoreply is on **or** its per-chat flag
|
|
|
-is on (`autoreply_enabled`, `src/app.rs:854-860`). `ai_setup`
|
|
|
|
|
-(`src/app.rs:983-989`) returns nothing, and so stops every path, when the AI
|
|
|
|
|
|
|
+is on (`autoreply_enabled`, `src/app.rs:904-910`). `ai_setup`
|
|
|
|
|
+(`src/app.rs:1033-1039`) returns nothing, and so stops every path, when the AI
|
|
|
client or the system prompt is missing. The full workflow is in
|
|
client or the system prompt is missing. The full workflow is in
|
|
|
[AI autoreply](../workflows/ai-autoreply.md); the state machine in `App` is:
|
|
[AI autoreply](../workflows/ai-autoreply.md); the state machine in `App` is:
|
|
|
|
|
|
|
@@ -191,58 +200,59 @@ flowchart TD
|
|
|
How a scheduled reply moves from the WhatsApp message to the AI request, with
|
|
How a scheduled reply moves from the WhatsApp message to the AI request, with
|
|
|
voice-note transcriptions always completing before image descriptions.
|
|
voice-note transcriptions always completing before image descriptions.
|
|
|
|
|
|
|
|
-- **Schedule** — `maybe_autoreply` (`src/app.rs:501-532`) calls
|
|
|
|
|
|
|
+- **Schedule** — `maybe_autoreply` (`src/app.rs:555-580`) calls
|
|
|
`schedule_reply` when `ReplyGate::should_schedule` allows it: the chat is
|
|
`schedule_reply` when `ReplyGate::should_schedule` allows it: the chat is
|
|
|
- marked pending and `spawn_reply_timer` (`src/app.rs:1581-1591`) emits
|
|
|
|
|
|
|
+ marked pending and `spawn_reply_timer` (`src/app.rs:1660-1670`) emits
|
|
|
`ReplyDue` after `reply_delay_seconds`. A chat already pending gets no second
|
|
`ReplyDue` after `reply_delay_seconds`. A chat already pending gets no second
|
|
|
timer, so messages arriving meanwhile are answered together. An image or an
|
|
timer, so messages arriving meanwhile are answered together. An image or an
|
|
|
audio sent by an allowed sender in an enabled chat is kept by `keep_image`
|
|
audio sent by an allowed sender in an enabled chat is kept by `keep_image`
|
|
|
- (`src/app.rs:544-564`) or `keep_audio` (`src/app.rs:583-609`): in
|
|
|
|
|
|
|
+ (`src/app.rs:596-612`) or `keep_audio` (`src/app.rs:636-657`): in
|
|
|
`pending_images`/`pending_audios` for a pending chat, in the backlog maps
|
|
`pending_images`/`pending_audios` for a pending chat, in the backlog maps
|
|
|
for an offline message before the catch-up, or processed at once without
|
|
for an offline message before the catch-up, or processed at once without
|
|
|
reply (`describe_late_image`, `transcribe_late_audio`,
|
|
reply (`describe_late_image`, `transcribe_late_audio`,
|
|
|
- `src/app.rs:566-626`) after it. A live image or audio always finds its chat
|
|
|
|
|
|
|
+ `src/app.rs:617-676`) after it. A live image or audio always finds its chat
|
|
|
pending, because scheduling runs first in `maybe_autoreply`; audios are kept
|
|
pending, because scheduling runs first in `maybe_autoreply`; audios are kept
|
|
|
only when transcription is configured.
|
|
only when transcription is configured.
|
|
|
-- **Reply due** — `handle_reply_due` (`src/app.rs:628-641`) takes the kept
|
|
|
|
|
|
|
+- **Reply due** — `handle_reply_due` (`src/app.rs:680-697`) takes the kept
|
|
|
images *and* audios and, if the chat is still pending, calls
|
|
images *and* audios and, if the chat is still pending, calls
|
|
|
- `prepare_reply` (`src/app.rs:643-679`). `reply_context`
|
|
|
|
|
- (`src/app.rs:862-880`) returns nothing when the autoreply was disabled, the
|
|
|
|
|
|
|
+ `prepare_reply` (`src/app.rs:702-729`). `reply_context`
|
|
|
|
|
+ (`src/app.rs:914-930`) returns nothing when the autoreply was disabled, the
|
|
|
AI is missing, the chat is empty or its last message was written by hand by
|
|
AI is missing, the chat is empty or its last message was written by hand by
|
|
|
the owner; then the pending state ends. Otherwise audios still lacking a
|
|
the owner; then the pending state ends. Otherwise audios still lacking a
|
|
|
transcription go first to `spawn_transcriptions`
|
|
transcription go first to `spawn_transcriptions`
|
|
|
- (`src/app.rs:681-715`), which passes the postponed images and
|
|
|
|
|
|
|
+ (`src/app.rs:734-763`), which passes the postponed images and
|
|
|
`then_reply: true`, and the reply follows `AudiosTranscribed`; with no
|
|
`then_reply: true`, and the reply follows `AudiosTranscribed`; with no
|
|
|
audios to transcribe, images still lacking a description go to
|
|
audios to transcribe, images still lacking a description go to
|
|
|
`spawn_descriptions` (reply follows on `ImagesDescribed`), or `start_reply`
|
|
`spawn_descriptions` (reply follows on `ImagesDescribed`), or `start_reply`
|
|
|
runs at once.
|
|
runs at once.
|
|
|
-- **Transcriptions** — `handle_audios_transcribed` (`src/app.rs:772-829`)
|
|
|
|
|
|
|
+- **Transcriptions** — `handle_audios_transcribed` (`src/app.rs:830-878`)
|
|
|
removes the ids from `transcribing` and appends each transcription to the
|
|
removes the ids from `transcribing` and appends each transcription to the
|
|
|
text of its message as `[messaggio vocale] <text>`
|
|
text of its message as `[messaggio vocale] <text>`
|
|
|
(`History::update_message_text`), sets one status for failures
|
|
(`History::update_message_text`), sets one status for failures
|
|
|
(`Trascrizione vocale fallita (N): <first error>`), reloads the open chat so
|
|
(`Trascrizione vocale fallita (N): <first error>`), reloads the open chat so
|
|
|
the text appears, then describes the postponed images (whose
|
|
the text appears, then describes the postponed images (whose
|
|
|
`ImagesDescribed` sends the reply) or starts the reply itself.
|
|
`ImagesDescribed` sends the reply) or starts the reply itself.
|
|
|
-- **Descriptions** — `handle_images_described` (`src/app.rs:738-770`) stores
|
|
|
|
|
|
|
+- **Descriptions** — `handle_images_described` (`src/app.rs:793-825`) stores
|
|
|
each description whose message still exists with
|
|
each description whose message still exists with
|
|
|
`History::save_image_description`, sets one status for failures
|
|
`History::save_image_description`, sets one status for failures
|
|
|
(`Descrizione immagine fallita (N): <first error>`) and, when `then_reply`
|
|
(`Descrizione immagine fallita (N): <first error>`) and, when `then_reply`
|
|
|
holds and the chat is still pending, starts the reply (or ends the pending
|
|
holds and the chat is still pending, starts the reply (or ends the pending
|
|
|
state if it is no longer wanted).
|
|
state if it is no longer wanted).
|
|
|
-- **Start** — `start_reply` (`src/app.rs:1000-1028`) records the last context
|
|
|
|
|
|
|
+- **Start** — `start_reply` (`src/app.rs:1056-1077`) records the last context
|
|
|
message in `answered_up_to`, marks own mentions on every context message,
|
|
message in `answered_up_to`, marks own mentions on every context message,
|
|
|
- reads the chat name and the stored image descriptions, builds the turn
|
|
|
|
|
- context (self-chat flag, group flag, chat name, descriptions), builds the
|
|
|
|
|
- turns and calls `spawn_ai_request`.
|
|
|
|
|
-- **Finish** — `finish_reply` (`src/app.rs:831-852`) clears the pending state
|
|
|
|
|
|
|
+ reads the chat name and the stored image descriptions, formats today's
|
|
|
|
|
+ local date with `today_label`, builds the turn context (self-chat flag,
|
|
|
|
|
+ group flag, chat name, descriptions, today's date), builds the turns and
|
|
|
|
|
+ calls `spawn_ai_request`.
|
|
|
|
|
+- **Finish** — `finish_reply` (`src/app.rs:883-900`) clears the pending state
|
|
|
and `answered_up_to`; if a newer message that passes `should_schedule`
|
|
and `answered_up_to`; if a newer message that passes `should_schedule`
|
|
|
arrived after the answered one, it schedules a new reply and **keeps** the
|
|
arrived after the answered one, it schedules a new reply and **keeps** the
|
|
|
images and audios received meanwhile; otherwise it drops the kept images and
|
|
images and audios received meanwhile; otherwise it drops the kept images and
|
|
|
audios of the chat.
|
|
audios of the chat.
|
|
|
-- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:920-930`)
|
|
|
|
|
|
|
+- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:970-980`)
|
|
|
runs `catch_up` once per process (`catch_up_done`). `catch_up`
|
|
runs `catch_up` once per process (`catch_up_done`). `catch_up`
|
|
|
- (`src/app.rs:932-962`) drains the backlog maps, asks `catch_up_chats`
|
|
|
|
|
- (`src/app.rs:964-981`) for the enabled chats (all chats when the global
|
|
|
|
|
|
|
+ (`src/app.rs:986-1010`) drains the backlog maps, asks `catch_up_chats`
|
|
|
|
|
+ (`src/app.rs:1015-1029`) for the enabled chats (all chats when the global
|
|
|
autoreply is on) whose latest stored message passes `ReplyGate::catch_up_due`
|
|
autoreply is on) whose latest stored message passes `ReplyGate::catch_up_due`
|
|
|
within `catch_up_seconds`, marks each pending and calls `prepare_reply` with
|
|
within `catch_up_seconds`, marks each pending and calls `prepare_reply` with
|
|
|
that chat's offline images and audios (no reply delay); the status becomes
|
|
that chat's offline images and audios (no reply delay); the status becomes
|
|
@@ -253,32 +263,31 @@ voice-note transcriptions always completing before image descriptions.
|
|
|
|
|
|
|
|
Voice notes take the same route as images, one step earlier:
|
|
Voice notes take the same route as images, one step earlier:
|
|
|
|
|
|
|
|
-- **Gate** — `transcription_enabled` (`src/app.rs:717-724`) is true only when
|
|
|
|
|
|
|
+- **Gate** — `transcription_enabled` (`src/app.rs:768-773`) is true only when
|
|
|
`transcribe_key` is set (URL, model and language always have configured
|
|
`transcribe_key` is set (URL, model and language always have configured
|
|
|
defaults). `keep_audio` returns immediately when it is not, because nothing
|
|
defaults). `keep_audio` returns immediately when it is not, because nothing
|
|
|
would consume the kept audio, and `audios_to_transcribe`
|
|
would consume the kept audio, and `audios_to_transcribe`
|
|
|
- (`src/app.rs:900-918`) yields nothing.
|
|
|
|
|
|
|
+ (`src/app.rs:952-966`) yields nothing.
|
|
|
- **Selection** — `audios_to_transcribe` shares `select_media_to_describe`
|
|
- **Selection** — `audios_to_transcribe` shares `select_media_to_describe`
|
|
|
- (`src/app.rs:1647-1676`, with `descriptions_apply`,
|
|
|
|
|
- `src/app.rs:1850-1857`) with images: it keeps the audios whose message is in
|
|
|
|
|
|
|
+ (`src/app.rs:1729-1752`, with `descriptions_apply`,
|
|
|
|
|
+ `src/app.rs:1929-1932`) with images: it keeps the audios whose message is in
|
|
|
the AI context, skips those already being transcribed or whose text is no
|
|
the AI context, skips those already being transcribed or whose text is no
|
|
|
longer the plain voice placeholder (that is, already transcribed), and keeps
|
|
longer the plain voice placeholder (that is, already transcribed), and keeps
|
|
|
at most the last `MAX_AUDIOS_PER_REPLY` (`3`, `src/app.rs:74-77`) in arrival
|
|
at most the last `MAX_AUDIOS_PER_REPLY` (`3`, `src/app.rs:74-77`) in arrival
|
|
|
order, dropping older ones for good to cap the cost of bursts. The
|
|
order, dropping older ones for good to cap the cost of bursts. The
|
|
|
equivalent image limit is `MAX_IMAGES_PER_REPLY` (`5`).
|
|
equivalent image limit is `MAX_IMAGES_PER_REPLY` (`5`).
|
|
|
-- **Spawning** — `spawn_transcriptions` (`src/app.rs:681-715`) builds a
|
|
|
|
|
|
|
+- **Spawning** — `spawn_transcriptions` (`src/app.rs:734-763`) builds a
|
|
|
`TranscriptionJob` from `transcribe_url`, `transcribe_key`,
|
|
`TranscriptionJob` from `transcribe_url`, `transcribe_key`,
|
|
|
`transcribe_model` and `transcribe_language` with an empty MIME type, adds
|
|
`transcribe_model` and `transcribe_language` with an empty MIME type, adds
|
|
|
the message ids to `transcribing` and calls `spawn_audio_transcriptions`
|
|
the message ids to `transcribing` and calls `spawn_audio_transcriptions`
|
|
|
- (`src/app.rs:1786-1830`): one task per audio run in parallel, each
|
|
|
|
|
|
|
+ (`src/app.rs:1870-1908`): one task per audio run in parallel, each
|
|
|
downloading the media with up to `MEDIA_DOWNLOAD_ATTEMPTS` (`3`) attempts,
|
|
downloading the media with up to `MEDIA_DOWNLOAD_ATTEMPTS` (`3`) attempts,
|
|
|
a `MEDIA_DOWNLOAD_TIMEOUT_SECONDS` (`60`) timeout each and growing retry
|
|
a `MEDIA_DOWNLOAD_TIMEOUT_SECONDS` (`60`) timeout each and growing retry
|
|
|
- waits (`download_audio_with_retry`, `src/app.rs:1758-1784`), then calling the
|
|
|
|
|
|
|
+ waits (`download_audio_with_retry`, `src/app.rs:1839-1866`), then calling the
|
|
|
OpenAI-compatible transcriber with the downloaded MIME type
|
|
OpenAI-compatible transcriber with the downloaded MIME type
|
|
|
- (`transcribe_one_audio`, `src/app.rs:1832-1848`, see
|
|
|
|
|
-<!-- openwiki: broken internal link [../integrations/audio-transcription.md] file "../integrations/audio-transcription.md" does not exist. Fix the href or restore the target, then delete this comment. -->
|
|
|
|
|
- [Audio transcription](../integrations/audio-transcription.md)); one
|
|
|
|
|
- collector task waits for all of them and emits a single
|
|
|
|
|
|
|
+ (`transcribe_one_audio`, `src/app.rs:1912-1926`, see
|
|
|
|
|
+ [AI client: audio transcription](../integrations/ai-client.md#audio-transcription));
|
|
|
|
|
+ one collector task waits for all of them and emits a single
|
|
|
`AudiosTranscribed`, a failed audio never stopping the others and the bytes
|
|
`AudiosTranscribed`, a failed audio never stopping the others and the bytes
|
|
|
staying in memory.
|
|
staying in memory.
|
|
|
- **Storage and statuses** — transcriptions are written by
|
|
- **Storage and statuses** — transcriptions are written by
|
|
@@ -287,7 +296,7 @@ Voice notes take the same route as images, one step earlier:
|
|
|
the audio download prefix is `Download audio fallito`
|
|
the audio download prefix is `Download audio fallito`
|
|
|
(`src/app.rs:70-71`) and the failure status is
|
|
(`src/app.rs:70-71`) and the failure status is
|
|
|
`Trascrizione vocale fallita (N): <first error>`
|
|
`Trascrizione vocale fallita (N): <first error>`
|
|
|
- (`transcription_failure_status`, `src/app.rs:1710-1718`).
|
|
|
|
|
|
|
+ (`transcription_failure_status`, `src/app.rs:1789-1796`).
|
|
|
|
|
|
|
|
### Ordering invariant
|
|
### Ordering invariant
|
|
|
|
|
|
|
@@ -303,7 +312,7 @@ message arrived after the answered one — otherwise both maps are emptied.
|
|
|
## Key bindings
|
|
## Key bindings
|
|
|
|
|
|
|
|
Commands use function keys (`src/app.rs:24-33`); `handle_key`
|
|
Commands use function keys (`src/app.rs:24-33`); `handle_key`
|
|
|
-(`src/app.rs:1122-1167`) dispatches them:
|
|
|
|
|
|
|
+(`src/app.rs:1179-1220`) dispatches them:
|
|
|
|
|
|
|
|
| Key | Where | Action |
|
|
| Key | Where | Action |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
@@ -323,12 +332,12 @@ Commands use function keys (`src/app.rs:24-33`); `handle_key`
|
|
|
| Delete (Canc) | list focus, chat mode | Ask to delete the selected chat; a second Delete confirms. |
|
|
| Delete (Canc) | list focus, chat mode | Ask to delete the selected chat; a second Delete confirms. |
|
|
|
| characters / Backspace | input focus | Edit the input line. |
|
|
| characters / Backspace | input focus | Edit the input line. |
|
|
|
| Esc | input focus | Return focus to the list. |
|
|
| Esc | input focus | Return focus to the list. |
|
|
|
-| Enter | input focus | Send the trimmed input (`submit_input`, `src/app.rs:1317-1329`). |
|
|
|
|
|
|
|
+| Enter | input focus | Send the trimmed input (`submit_input`, `src/app.rs:1370-1382`). |
|
|
|
|
|
|
|
|
F10 and F8 are checked first; any other key cancels a pending relink (Esc is
|
|
F10 and F8 are checked first; any other key cancels a pending relink (Esc is
|
|
|
then consumed). Non-main screens ignore every remaining key. Ctrl chords are
|
|
then consumed). Non-main screens ignore every remaining key. Ctrl chords are
|
|
|
swallowed (they cancel a pending deletion and do nothing else) so they never
|
|
swallowed (they cancel a pending deletion and do nothing else) so they never
|
|
|
-type into the input or the filter; `is_command` (`src/app.rs:1932-1939`)
|
|
|
|
|
|
|
+type into the input or the filter; `is_command` (`src/app.rs:2032-2039`)
|
|
|
treats Ctrl as a command only when Alt is not held, because Windows reports
|
|
treats Ctrl as a command only when Alt is not held, because Windows reports
|
|
|
AltGr as Ctrl+Alt (needed for `@`, `#`, `€` on the Italian layout). Switching
|
|
AltGr as Ctrl+Alt (needed for `@`, `#`, `€` on the Italian layout). Switching
|
|
|
list mode with F2 also makes the event loop repaint the whole terminal (see
|
|
list mode with F2 also makes the event loop repaint the whole terminal (see
|
|
@@ -337,119 +346,145 @@ list mode with F2 also makes the event loop repaint the whole terminal (see
|
|
|
### List filter
|
|
### List filter
|
|
|
|
|
|
|
|
Typing while the list has focus edits `filter` (`handle_list_key`,
|
|
Typing while the list has focus edits `filter` (`handle_list_key`,
|
|
|
-`edit_filter`, `src/app.rs:1169-1211`). `visible_chats` / `visible_contacts`
|
|
|
|
|
-(`src/app.rs:1285-1299`) keep the entries whose name contains the filter,
|
|
|
|
|
-case-insensitively (`matches_filter`, `src/app.rs:1526-1532`); selection,
|
|
|
|
|
|
|
+`edit_filter`, `src/app.rs:1224-1263`). `visible_chats` / `visible_contacts`
|
|
|
|
|
+(`src/app.rs:1338-1352`) keep the entries whose name contains the filter,
|
|
|
|
|
+case-insensitively (`matches_filter`, `src/app.rs:1604-1608`); selection,
|
|
|
opening, F3 and deletion all work on the filtered list.
|
|
opening, F3 and deletion all work on the filtered list.
|
|
|
|
|
|
|
|
### Local chat deletion
|
|
### Local chat deletion
|
|
|
|
|
|
|
|
-`request_delete` (`src/app.rs:1225-1247`) works only in chat mode: the first
|
|
|
|
|
|
|
+`request_delete` (`src/app.rs:1279-1300`) works only in chat mode: the first
|
|
|
Delete stores the selected chat in `pending_delete` and shows
|
|
Delete stores the selected chat in `pending_delete` and shows
|
|
|
`Eliminare la chat <name>? Canc conferma, Esc annulla`; a second Delete on
|
|
`Eliminare la chat <name>? Canc conferma, Esc annulla`; a second Delete on
|
|
|
-the same chat calls `confirm_delete` (`src/app.rs:1249-1272`), which removes
|
|
|
|
|
|
|
+the same chat calls `confirm_delete` (`src/app.rs:1304-1324`), which removes
|
|
|
the chat from the history (messages, image descriptions and row, hence also
|
|
the chat from the history (messages, image descriptions and row, hence also
|
|
|
its per-chat flag; no WhatsApp call, the contact is kept), takes its pending
|
|
its per-chat flag; no WhatsApp call, the contact is kept), takes its pending
|
|
|
reply, drops its pending and backlog images, pending and backlog audios and
|
|
reply, drops its pending and backlog images, pending and backlog audios and
|
|
|
answered snapshot, closes it if open and shows `Chat <name> eliminata`. Any
|
|
answered snapshot, closes it if open and shows `Chat <name> eliminata`. Any
|
|
|
other key (Tab, F2, F3, F4, F8, list keys, Ctrl chords) cancels the pending
|
|
other key (Tab, F2, F3, F4, F8, list keys, Ctrl chords) cancels the pending
|
|
|
deletion with `Eliminazione annullata` (`cancel_pending_delete`,
|
|
deletion with `Eliminazione annullata` (`cancel_pending_delete`,
|
|
|
-`src/app.rs:1213-1223`).
|
|
|
|
|
|
|
+`src/app.rs:1266-1275`).
|
|
|
|
|
|
|
|
### Opening and toggling
|
|
### Opening and toggling
|
|
|
|
|
|
|
|
-`selected_jid` (`src/app.rs:1462-1473`) returns the JID on the selected row of
|
|
|
|
|
|
|
+`selected_jid` (`src/app.rs:1540-1550`) returns the JID on the selected row of
|
|
|
the active filtered list (chat or contact), or nothing when it is empty.
|
|
the active filtered list (chat or contact), or nothing when it is empty.
|
|
|
-`open_selected` (`src/app.rs:1446-1460`) uses it to load display name,
|
|
|
|
|
|
|
+`open_selected` (`src/app.rs:1524-1537`) uses it to load display name,
|
|
|
per-chat autoreply flag and the last `MESSAGES_SHOWN` (200) messages, then
|
|
per-chat autoreply flag and the last `MESSAGES_SHOWN` (200) messages, then
|
|
|
moves focus to the input. The `reload_*` helpers
|
|
moves focus to the input. The `reload_*` helpers
|
|
|
-(`src/app.rs:1486-1515`) keep `selected` inside the active filtered list.
|
|
|
|
|
|
|
+(`src/app.rs:1564-1593`) keep `selected` inside the active filtered list.
|
|
|
|
|
|
|
|
-`toggle_autoreply` (F3, `src/app.rs:1342-1357`) picks the target from the
|
|
|
|
|
|
|
+`toggle_autoreply` (F3, `src/app.rs:1397-1411`) picks the target from the
|
|
|
focus — `selected_jid` with list focus, `open_chat` with input focus — and
|
|
focus — `selected_jid` with list focus, `open_chat` with input focus — and
|
|
|
-delegates to `toggle_chat_autoreply` (`src/app.rs:1359-1378`), which always
|
|
|
|
|
|
|
+delegates to `toggle_chat_autoreply` (`src/app.rs:1414-1427`), which always
|
|
|
allows disabling and updates `open_chat_autoreply` when the target is the open
|
|
allows disabling and updates `open_chat_autoreply` when the target is the open
|
|
|
chat. Enabling goes through `can_enable_autoreply`
|
|
chat. Enabling goes through `can_enable_autoreply`
|
|
|
-(`src/app.rs:1889-1902`), which first requires an AI client and then a chat
|
|
|
|
|
|
|
+(`src/app.rs:1968-1976`), which first requires an AI client and then a chat
|
|
|
accepted by `supports_autoreply` (private chats and groups; broadcasts,
|
|
accepted by `supports_autoreply` (private chats and groups; broadcasts,
|
|
|
status and newsletters are refused with
|
|
status and newsletters are refused with
|
|
|
`Autoreply disponibile solo nelle chat private e nei gruppi`). The flag is
|
|
`Autoreply disponibile solo nelle chat private e nei gruppi`). The flag is
|
|
|
persisted with `History::set_autoreply` and the status shows
|
|
persisted with `History::set_autoreply` and the status shows
|
|
|
`Autoreply ON|OFF per <name>`.
|
|
`Autoreply ON|OFF per <name>`.
|
|
|
|
|
|
|
|
-`toggle_global_autoreply` (F4, `src/app.rs:1427-1444`) refuses to enable
|
|
|
|
|
|
|
+`toggle_global_autoreply` (F4, `src/app.rs:1507-1522`) refuses to enable
|
|
|
without an AI client (`STATUS_AI_NOT_CONFIGURED`), persists the flag with
|
|
without an AI client (`STATUS_AI_NOT_CONFIGURED`), persists the flag with
|
|
|
`History::set_global_autoreply`, updates `global_autoreply` and shows
|
|
`History::set_global_autoreply`, updates `global_autoreply` and shows
|
|
|
`Autoreply globale ON|OFF`. Disabling is always allowed.
|
|
`Autoreply globale ON|OFF`. Disabling is always allowed.
|
|
|
|
|
|
|
|
## Web panel requests
|
|
## Web panel requests
|
|
|
|
|
|
|
|
-`handle_web_request` (`src/app.rs:1380-1397`) applies a `WebCommand` and then
|
|
|
|
|
-always sends back a `WebState` snapshot on the request's oneshot channel:
|
|
|
|
|
|
|
+`handle_web_request` (`src/app.rs:1434-1447`) applies a `WebCommand`, then
|
|
|
|
|
+calls `maybe_start_avatar_fetch` and always sends back a `WebState` snapshot
|
|
|
|
|
+on the request's oneshot channel:
|
|
|
|
|
|
|
|
- `State` → nothing, just the snapshot;
|
|
- `State` → nothing, just the snapshot;
|
|
|
- `ToggleGlobal` → `toggle_global_autoreply` (same rules as F4);
|
|
- `ToggleGlobal` → `toggle_global_autoreply` (same rules as F4);
|
|
|
-- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1399-1410`): if the JID is
|
|
|
|
|
|
|
+- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1478-1484`): if the JID is
|
|
|
not among the panel's chats the status becomes `Chat non trovata`,
|
|
not among the panel's chats the status becomes `Chat non trovata`,
|
|
|
otherwise `toggle_chat_autoreply` (same rules as F3);
|
|
otherwise `toggle_chat_autoreply` (same rules as F3);
|
|
|
- `Relink` → `start_relink` (no second confirmation; ignored while a relink runs);
|
|
- `Relink` → `start_relink` (no second confirmation; ignored while a relink runs);
|
|
|
- `Quit` → `should_quit = true`.
|
|
- `Quit` → `should_quit = true`.
|
|
|
|
|
|
|
|
-`web_state` (`src/app.rs:1412-1425`) returns the global flag, the toggleable
|
|
|
|
|
-chats (`web_chats`, `src/app.rs:1916-1930` — the TUI chat list in the same
|
|
|
|
|
-order, restricted to chats that support autoreply), the status line, the
|
|
|
|
|
-current screen as `connecting` / `pairing` / `main` (`web_screen`,
|
|
|
|
|
-`src/app.rs:1904-1914`) and, while a QR payload is stored, the QR code
|
|
|
|
|
-rendered as SVG by `web::qr_svg`. See
|
|
|
|
|
|
|
+`web_state` (`src/app.rs:1491-1499`) returns the global flag, the toggleable
|
|
|
|
|
+chats (`web_chats`, `src/app.rs:1995-2006` — the TUI chat list in the same
|
|
|
|
|
+order, restricted to chats that support autoreply, each with its cached avatar
|
|
|
|
|
+URL), the status line, the current screen as `connecting` / `pairing` /
|
|
|
|
|
+`main` (`web_screen`, `src/app.rs:1982-1988`) and, while a QR payload is
|
|
|
|
|
+stored, the QR code rendered as SVG by `web::qr_svg`. See
|
|
|
[Web control panel](../integrations/web-control-panel.md).
|
|
[Web control panel](../integrations/web-control-panel.md).
|
|
|
|
|
|
|
|
|
|
+### Avatar cache
|
|
|
|
|
+
|
|
|
|
|
+Profile pictures for the web panel are fetched lazily, driven by the panel's
|
|
|
|
|
+own polling, and live only in memory:
|
|
|
|
|
+
|
|
|
|
|
+- **Trigger** — `maybe_start_avatar_fetch` (`src/app.rs:1456-1471`) does
|
|
|
|
|
+ nothing unless the screen is `Main`, `wa_connected` is true and no fetch is
|
|
|
|
|
+ running. It collects the JIDs of the web chats whose avatar is missing or
|
|
|
|
|
+ older than `AVATAR_TTL_SECONDS` (86 400 s, one day, `src/app.rs:116-119`)
|
|
|
|
|
+ and, when any, sets `avatar_fetch_running` and spawns `fetch_avatars`.
|
|
|
|
|
+- **Fetch** — `fetch_avatars` (`src/app.rs:2015-2030`) asks
|
|
|
|
|
+ `WaHandle::profile_picture_url` for one chat at a time, sleeping
|
|
|
|
|
+ `AVATAR_FETCH_INTERVAL_SECONDS` (2 s) between requests to avoid WhatsApp
|
|
|
|
|
+ rate limits, and sends `AvatarFetched` per chat. On the first error it
|
|
|
|
|
+ caches that chat without a picture (so it is not retried before the TTL),
|
|
|
|
|
+ stops and sends `AvatarFetchFinished` with the error; otherwise it ends with
|
|
|
|
|
+ `AvatarFetchFinished { error: None }`.
|
|
|
|
|
+- **Storage** — `AvatarFetched` inserts a `CachedAvatar` stamped with the
|
|
|
|
|
+ current `Instant`; `AvatarFetchFinished` clears `avatar_fetch_running` and,
|
|
|
|
|
+ on error, shows `Foto profilo non caricate: <error>`. A "no picture" entry
|
|
|
|
|
+ expires after the same TTL, which therefore doubles as the retry delay after
|
|
|
|
|
+ a failure. The cache is not persisted, so it is rebuilt after each restart.
|
|
|
|
|
+
|
|
|
## Sending and background tasks
|
|
## Sending and background tasks
|
|
|
|
|
|
|
|
All network work runs in tokio tasks that report back through the event
|
|
All network work runs in tokio tasks that report back through the event
|
|
|
channel; `send_event` ignores a closed channel because that only happens on
|
|
channel; `send_event` ignores a closed channel because that only happens on
|
|
|
-shutdown (`src/app.rs:1572-1579`).
|
|
|
|
|
|
|
+shutdown (`src/app.rs:1650-1657`).
|
|
|
|
|
|
|
|
-- `submit_input` (`src/app.rs:1317-1329`) ignores blank input or no open chat,
|
|
|
|
|
|
|
+- `submit_input` (`src/app.rs:1370-1382`) ignores blank input or no open chat,
|
|
|
clears the input and calls `spawn_send` with `is_ai: false`.
|
|
clears the input and calls `spawn_send` with `is_ai: false`.
|
|
|
-- `spawn_send` (`src/app.rs:1859-1872`) calls `WaHandle::send_text` and emits
|
|
|
|
|
|
|
+- `spawn_send` (`src/app.rs:1937-1950`) calls `WaHandle::send_text` and emits
|
|
|
`Sent`; during a relink no session is running, so sends fail with a send
|
|
`Sent`; during a relink no session is running, so sends fail with a send
|
|
|
error.
|
|
error.
|
|
|
-- `handle_sent` (`src/app.rs:1092-1120`) stores a successful send as a
|
|
|
|
|
|
|
+- `handle_sent` (`src/app.rs:1145-1175`) stores a successful send as a
|
|
|
`from_me` message (sender `OWN_SENDER_JID` = empty, `is_ai` preserved) and
|
|
`from_me` message (sender `OWN_SENDER_JID` = empty, `is_ai` preserved) and
|
|
|
reloads the views; a failure sets `Invio fallito: …`.
|
|
reloads the views; a failure sets `Invio fallito: …`.
|
|
|
-- `spawn_ai_request` (`src/app.rs:1593-1611`) spawns the HTTP request task and
|
|
|
|
|
|
|
+- `spawn_ai_request` (`src/app.rs:1674-1692`) spawns the HTTP request task and
|
|
|
a waiter that always sends `AiReply`, also when the request task fails
|
|
a waiter that always sends `AiReply`, also when the request task fails
|
|
|
unexpectedly, so the chat never stays pending.
|
|
unexpectedly, so the chat never stays pending.
|
|
|
-- `spawn_image_descriptions` (`src/app.rs:1613-1645`) runs one task per image
|
|
|
|
|
|
|
+- `spawn_image_descriptions` (`src/app.rs:1696-1725`) runs one task per image
|
|
|
in parallel (download with up to 3 attempts and a 60 s timeout each, then
|
|
in parallel (download with up to 3 attempts and a 60 s timeout each, then
|
|
|
`AiClient::describe_image`) and a collector task that emits a single
|
|
`AiClient::describe_image`) and a collector task that emits a single
|
|
|
`ImagesDescribed`; a failed image does not stop the others and the bytes
|
|
`ImagesDescribed`; a failed image does not stop the others and the bytes
|
|
|
stay in memory.
|
|
stay in memory.
|
|
|
-- `spawn_audio_transcriptions` (`src/app.rs:1786-1830`) does the same for
|
|
|
|
|
|
|
+- `spawn_audio_transcriptions` (`src/app.rs:1870-1908`) does the same for
|
|
|
audios, emitting a single `AudiosTranscribed` that also carries the
|
|
audios, emitting a single `AudiosTranscribed` that also carries the
|
|
|
postponed images (see [Audio transcription path](#audio-transcription-path)).
|
|
postponed images (see [Audio transcription path](#audio-transcription-path)).
|
|
|
-- `handle_ai_reply` (`src/app.rs:1044-1056`) shows
|
|
|
|
|
|
|
+- `handle_ai_reply` (`src/app.rs:1099-1110`) shows
|
|
|
`AI: nessuna risposta necessaria` for `Reply::NoReply`, `Errore AI: …` for
|
|
`AI: nessuna risposta necessaria` for `Reply::NoReply`, `Errore AI: …` for
|
|
|
an error, and passes text to `send_ai_reply_if_enabled`
|
|
an error, and passes text to `send_ai_reply_if_enabled`
|
|
|
- (`src/app.rs:1058-1077`), which sends it only if the chat is still enabled
|
|
|
|
|
|
|
+ (`src/app.rs:1113-1130`), which sends it only if the chat is still enabled
|
|
|
and the owner did not answer by hand meanwhile (`owner_answered_meanwhile`,
|
|
and the owner did not answer by hand meanwhile (`owner_answered_meanwhile`,
|
|
|
- `src/app.rs:1079-1090`); otherwise it shows
|
|
|
|
|
|
|
+ `src/app.rs:1133-1142`); otherwise it shows
|
|
|
`Risposta AI scartata: autoreply disattivato` or
|
|
`Risposta AI scartata: autoreply disattivato` or
|
|
|
`Risposta AI scartata: hai già risposto`.
|
|
`Risposta AI scartata: hai già risposto`.
|
|
|
- The relink task (see [Relink](#relink)) runs `WaHandle::relink` and emits
|
|
- The relink task (see [Relink](#relink)) runs `WaHandle::relink` and emits
|
|
|
`Relinked`.
|
|
`Relinked`.
|
|
|
|
|
+- The avatar fetch task (see [Avatar cache](#avatar-cache)) emits
|
|
|
|
|
+ `AvatarFetched` and `AvatarFetchFinished`.
|
|
|
|
|
|
|
|
## Tests
|
|
## Tests
|
|
|
|
|
|
|
|
-Unit tests at `src/app.rs:1941` onward cover `can_enable_autoreply` (private,
|
|
|
|
|
|
|
+Unit tests at `src/app.rs:2041` onward cover `can_enable_autoreply` (private,
|
|
|
self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
|
|
self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
|
|
|
-`web_chats` filtering and order, `descriptions_apply`,
|
|
|
|
|
-`select_media_to_describe` (last five images and last three audios, skip known
|
|
|
|
|
-and out-of-context, empty when not enabled), `arrived_after`,
|
|
|
|
|
-`manual_reply_after`, `description_failure_status`,
|
|
|
|
|
|
|
+`web_chats` filtering and order (with an empty avatar cache),
|
|
|
|
|
+`descriptions_apply`, `select_media_to_describe` (last five images and last
|
|
|
|
|
+three audios, skip known and out-of-context, empty when not enabled),
|
|
|
|
|
+`arrived_after`, `manual_reply_after`, `description_failure_status`,
|
|
|
`transcription_failure_status`, the voice-note text, `matches_filter`,
|
|
`transcription_failure_status`, the voice-note text, `matches_filter`,
|
|
|
selection clamping/stepping and page steps, the toggle helpers, `is_command`
|
|
selection clamping/stepping and page steps, the toggle helpers, `is_command`
|
|
|
and the fixed function-key codes (F2, F3, F4, F10; F8 is not part of that
|
|
and the fixed function-key codes (F2, F3, F4, F10; F8 is not part of that
|
|
|
-check). Relink, configuration reload, the transcription handlers and F3
|
|
|
|
|
-targeting have no `App`-level tests. See
|
|
|
|
|
|
|
+check). Relink, configuration reload, the transcription handlers, the avatar
|
|
|
|
|
+cache and F3 targeting have no `App`-level tests. See
|
|
|
[Unit tests](../testing/unit-tests.md).
|
|
[Unit tests](../testing/unit-tests.md).
|