|
|
@@ -1,15 +1,15 @@
|
|
|
---
|
|
|
type: architecture
|
|
|
title: Application state and event handling
|
|
|
-description: How the App struct in src/app.rs holds UI state, routes AppEvent values (keys, WhatsApp, AI replies, image descriptions, reply timers, web panel), handles function keys, the list filter and chat deletion, toggles global and per-chat autoreply, and runs background tasks.
|
|
|
-tags: [app, state, events, keybindings, tokio, web-panel]
|
|
|
+description: How the App struct in src/app.rs holds UI state, routes AppEvent values (keys, WhatsApp, AI replies, image descriptions, reply timers, web panel, configuration reloads, relink outcomes), handles function keys (including F8 relink), the list filter and chat deletion, toggles global and per-chat autoreply, and runs background tasks.
|
|
|
+tags: [app, state, events, keybindings, tokio, web-panel, relink, config-reload]
|
|
|
verified:
|
|
|
- by: openwiki/0.7.2
|
|
|
- at: 2026-10-21T21:49:12.291Z
|
|
|
+ at: 2026-10-23T21:26:24.156Z
|
|
|
sources:
|
|
|
- id: openwiki-source-9fec9d1af408501c9c455161
|
|
|
resource: repo://src/app.rs
|
|
|
-generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" }
|
|
|
+generated: { by: "claude-code", at: "2026-10-23T21:26:24.156Z" }
|
|
|
---
|
|
|
|
|
|
# Application state and event handling
|
|
|
@@ -23,7 +23,7 @@ and [web panel](../integrations/web-control-panel.md) activity.
|
|
|
|
|
|
## Events
|
|
|
|
|
|
-`AppEvent` (`src/app.rs:90-121`) is the single message type of the main channel:
|
|
|
+`AppEvent` (`src/app.rs:106-141`) is the single message type of the main channel:
|
|
|
|
|
|
| Variant | Producer | Meaning |
|
|
|
|---|---|---|
|
|
|
@@ -35,15 +35,17 @@ and [web panel](../integrations/web-control-panel.md) activity.
|
|
|
| `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. |
|
|
|
| `Web(WebRequest)` | `wh-web` thread | A command from the [web control panel](../integrations/web-control-panel.md) with a reply channel. |
|
|
|
+| `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. |
|
|
|
|
|
|
-`handle_event` (`src/app.rs:267-283`) dispatches each variant; after an
|
|
|
+`handle_event` (`src/app.rs:293-317`) dispatches each variant; after an
|
|
|
`AiReply` it always calls `finish_reply` to end the chat's pending state. Only
|
|
|
fatal history (SQLite) errors propagate as `Err`; every other failure is turned
|
|
|
into a `status` text shown in the footer.
|
|
|
|
|
|
## State
|
|
|
|
|
|
-`App` (`src/app.rs:169-215`) exposes UI fields read by the renderer: `screen`,
|
|
|
+`App` (`src/app.rs:188-239`) exposes UI fields read by the renderer: `screen`,
|
|
|
`qr_code`, `focus`, `list_mode`, `chats`, `contacts`, `selected`, `filter`,
|
|
|
`open_chat`, `open_chat_name`, `open_chat_autoreply`, `global_autoreply`,
|
|
|
`messages`, `input`, `status` and `should_quit`. Private fields hold the
|
|
|
@@ -59,36 +61,50 @@ bookkeeping:
|
|
|
| `answered_up_to` | per chat, the id of the last message in the context of the reply in progress |
|
|
|
| `own_jids_known`, `catch_up_done` | one-shot flags for own JIDs and the startup catch-up |
|
|
|
| `pending_delete` | the chat awaiting a deletion confirmation |
|
|
|
+| `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 |
|
|
|
|
|
|
-- `Screen` (`src/app.rs:131-137`): `Connecting` → `Pairing` (QR shown) → `Main`.
|
|
|
-- `Focus` (`src/app.rs:139-144`): `List` or `Input`.
|
|
|
-- `ListMode` (`src/app.rs:146-151`): `Chats` or `Contacts`.
|
|
|
+- `Screen` (`src/app.rs:151-157`): `Connecting` → `Pairing` (QR shown) → `Main`.
|
|
|
+- `Focus` (`src/app.rs:159-164`): `List` or `Input`.
|
|
|
+- `ListMode` (`src/app.rs:166-171`): `Chats` or `Contacts`.
|
|
|
|
|
|
-`App::new` (`src/app.rs:222-260`) builds the AI client with `build_ai`, loads
|
|
|
+`App::new` (`src/app.rs:246-286`) builds the AI client with `build_ai`, loads
|
|
|
chats, contacts and the persisted global autoreply flag from history, starts on
|
|
|
-`Connecting` with list focus in chat mode and an empty filter, and creates the
|
|
|
-`ReplyGate` with the current time, so that backlog messages are never
|
|
|
-auto-answered live.
|
|
|
+`Connecting` with list focus in chat mode, an empty filter and no pending
|
|
|
+deletion or relink, and creates the `ReplyGate` with the current time, so that
|
|
|
+backlog messages are never auto-answered live.
|
|
|
|
|
|
-`build_ai` (`src/app.rs:1393-1401`) returns no client when the system prompt
|
|
|
+`build_ai` (`src/app.rs:1534-1542`) returns no client when the system prompt
|
|
|
(`SOUL.md`, see [Configuration](../operations/configuration-and-data.md)) is
|
|
|
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
|
|
|
`AI non disponibile` prefix.
|
|
|
|
|
|
+## Configuration reload
|
|
|
+
|
|
|
+The [configuration watcher](../operations/configuration-and-data.md#live-reload)
|
|
|
+sends `ConfigReloaded` when `config.toml`, `SOUL.md` or `VISION.md` change.
|
|
|
+`apply_reloaded_config` (`src/app.rs:388-400`) keeps the current configuration
|
|
|
+and shows `Configurazione non ricaricata: <error>` when the load failed;
|
|
|
+otherwise it replaces `config`, rebuilds the AI client with `build_ai` (replies
|
|
|
+already running keep the client they were started with) and shows
|
|
|
+`Configurazione ricaricata`, or the `build_ai` status when the AI is not
|
|
|
+configured or unavailable. Later replies therefore use the new prompt, model,
|
|
|
+delays and retries without a restart.
|
|
|
+
|
|
|
## WhatsApp events
|
|
|
|
|
|
-`handle_wa_event` (`src/app.rs:289-312`):
|
|
|
+`handle_wa_event` (`src/app.rs:406-429`):
|
|
|
|
|
|
- `Qr` → switch to `Pairing` and store the QR payload.
|
|
|
-- `Connected` → switch to `Main`, status `Connesso`, refresh own JIDs.
|
|
|
+- `Connected` → switch to `Main`, clear the QR, status `Connesso`, refresh own JIDs.
|
|
|
- `Disconnected` / `Error` → update `status` only.
|
|
|
- `LoggedOut` → back to `Connecting`, status `Sessione scollegata`.
|
|
|
- `OfflineSyncCompleted` → `on_offline_sync_completed`, the startup catch-up
|
|
|
(see below).
|
|
|
- `Contact` → `on_contact`: upsert the contact, reload contacts and chats and
|
|
|
- refresh the open chat name (`src/app.rs:318-326`).
|
|
|
-- `Message { message, image }` → `on_message` (`src/app.rs:333-342`).
|
|
|
+ refresh the open chat name (`src/app.rs:435-443`).
|
|
|
+- `Message { message, image }` → `on_message` (`src/app.rs:450-459`).
|
|
|
|
|
|
`on_message` saves the message and stops immediately when
|
|
|
`History::save_message` reports a duplicate, so history sync replays never
|
|
|
@@ -96,50 +112,74 @@ trigger UI reloads or autoreplies twice. New messages reload the chat list, the
|
|
|
open conversation and then go through `maybe_autoreply` (see
|
|
|
[AI autoreply](../workflows/ai-autoreply.md)).
|
|
|
|
|
|
-Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:680-687`); the list is
|
|
|
-empty while the device is not paired, so the refresh is retried later
|
|
|
-(`own_jids_known` stays false until it succeeds).
|
|
|
+Own JIDs are fetched by `refresh_own_jids` (`src/app.rs:797-804`); the list is
|
|
|
+empty while the device is not paired (or no session is running during a
|
|
|
+relink), so the refresh is retried later (`own_jids_known` stays false until it
|
|
|
+succeeds).
|
|
|
+
|
|
|
+## Relink
|
|
|
+
|
|
|
+F8 (twice) and the web panel's `Relink` command replace the WhatsApp session
|
|
|
+with a new unpaired one; the full flow is in
|
|
|
+[WhatsApp relink and pairing](../workflows/relink-and-pairing.md). In `App`:
|
|
|
+
|
|
|
+- `request_relink` (`src/app.rs:361-369`) cancels a pending chat deletion; the
|
|
|
+ first F8 sets `pending_relink` and shows
|
|
|
+ `Cancellare la sessione WhatsApp e rifare l'abbinamento? F8 conferma, Esc annulla`,
|
|
|
+ the second calls `start_relink`.
|
|
|
+- `cancel_pending_relink` (`src/app.rs:375-381`) clears the flag on any other
|
|
|
+ key and shows `Nuovo abbinamento annullato`.
|
|
|
+- `start_relink` (`src/app.rs:326-341`) is a no-op while `relinking`;
|
|
|
+ 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
|
|
|
+ `WaHandle::relink` and sends `Relinked`. The web command calls it directly,
|
|
|
+ without the two-press confirmation (the browser asks instead).
|
|
|
+- `finish_relink` (`src/app.rs:348-354`) clears `relinking` and shows
|
|
|
+ `Sessione cancellata: scansiona il nuovo QR` or
|
|
|
+ `Nuovo abbinamento fallito: <error>`; the screen then follows the new
|
|
|
+ session's `Qr` / `Connected` events.
|
|
|
|
|
|
## Autoreply orchestration
|
|
|
|
|
|
A chat is "enabled" when the global autoreply is on **or** its per-chat flag
|
|
|
-is on (`autoreply_enabled`, `src/app.rs:525-527`). `ai_setup`
|
|
|
-(`src/app.rs:632-634`) returns nothing, and so stops every path, when the AI
|
|
|
+is on (`autoreply_enabled`, `src/app.rs:642-644`). `ai_setup`
|
|
|
+(`src/app.rs:749-751`) returns nothing, and so stops every path, when the AI
|
|
|
client or the system prompt is missing. The full workflow is in
|
|
|
[AI autoreply](../workflows/ai-autoreply.md); the state machine in `App` is:
|
|
|
|
|
|
-- **Schedule** — `maybe_autoreply` (`src/app.rs:351-366`) calls
|
|
|
+- **Schedule** — `maybe_autoreply` (`src/app.rs:468-483`) calls
|
|
|
`schedule_reply` when `ReplyGate::should_schedule` allows it: the chat is
|
|
|
- marked pending and `spawn_reply_timer` (`src/app.rs:1204-1209`) emits
|
|
|
+ marked pending and `spawn_reply_timer` (`src/app.rs:1345-1350`) emits
|
|
|
`ReplyDue` after `reply_delay_seconds`. A chat already pending gets no second
|
|
|
timer, so messages arriving meanwhile are answered together. An image sent by
|
|
|
an allowed sender in an enabled chat is kept by `keep_image`
|
|
|
- (`src/app.rs:384-398`): in `pending_images` for a pending chat, in
|
|
|
+ (`src/app.rs:501-515`): in `pending_images` for a pending chat, in
|
|
|
`backlog_images` for an offline image before the catch-up, or described at
|
|
|
once without reply (`describe_late_image`) after it.
|
|
|
-- **Reply due** — `handle_reply_due` (`src/app.rs:422-428`) takes the kept
|
|
|
+- **Reply due** — `handle_reply_due` (`src/app.rs:539-545`) takes the kept
|
|
|
images and, if the chat is still pending, calls `prepare_reply`
|
|
|
- (`src/app.rs:439-451`). `reply_context` (`src/app.rs:535-547`) returns
|
|
|
+ (`src/app.rs:556-568`). `reply_context` (`src/app.rs:652-664`) returns
|
|
|
nothing when the autoreply was disabled, the AI is missing, the chat is
|
|
|
empty or its last message was written by hand by the owner; then the
|
|
|
pending state ends. Otherwise images still lacking a description go to
|
|
|
`spawn_descriptions` (reply follows on `ImagesDescribed`), or `start_reply`
|
|
|
runs at once.
|
|
|
-- **Descriptions** — `handle_images_described` (`src/app.rs:474-497`) stores
|
|
|
+- **Descriptions** — `handle_images_described` (`src/app.rs:591-614`) stores
|
|
|
each description whose message still exists with
|
|
|
`History::save_image_description`, sets one status for failures
|
|
|
(`Descrizione immagine fallita (N): <first error>`) and, when `then_reply`
|
|
|
holds and the chat is still pending, starts the reply.
|
|
|
-- **Start** — `start_reply` (`src/app.rs:654-673`) records the last context
|
|
|
+- **Start** — `start_reply` (`src/app.rs:771-790`) records the last context
|
|
|
message in `answered_up_to`, marks own mentions, reads the chat name and the
|
|
|
stored image descriptions, builds the turns and calls `spawn_ai_request`.
|
|
|
-- **Finish** — `finish_reply` (`src/app.rs:505-519`) clears the pending state
|
|
|
+- **Finish** — `finish_reply` (`src/app.rs:622-636`) clears the pending state
|
|
|
and `answered_up_to`; if a newer message that passes `should_schedule`
|
|
|
arrived after the answered one, it schedules a new reply (keeping its
|
|
|
images), otherwise it drops the kept images.
|
|
|
-- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:571-577`)
|
|
|
+- **Startup catch-up** — `on_offline_sync_completed` (`src/app.rs:688-694`)
|
|
|
runs `catch_up` once per process (`catch_up_done`). `catch_up`
|
|
|
- (`src/app.rs:587-607`) asks `catch_up_chats` (`src/app.rs:614-626`) for the
|
|
|
+ (`src/app.rs:704-724`) asks `catch_up_chats` (`src/app.rs:731-743`) for the
|
|
|
enabled chats (all chats when the global autoreply is on) whose latest
|
|
|
stored message passes `ReplyGate::catch_up_due` within `catch_up_seconds`,
|
|
|
marks each pending and calls `prepare_reply` with its offline images (no
|
|
|
@@ -148,13 +188,15 @@ client or the system prompt is missing. The full workflow is in
|
|
|
## Key bindings
|
|
|
|
|
|
Commands use function keys (`src/app.rs:23-32`); `handle_key`
|
|
|
-(`src/app.rs:774-804`) dispatches them:
|
|
|
+(`src/app.rs:884-929`) dispatches them:
|
|
|
|
|
|
| Key | Where | Action |
|
|
|
|---|---|---|
|
|
|
| F10 | any screen | Quit (`should_quit = true`). |
|
|
|
+| F8 | any screen | First press asks to delete the WhatsApp session and pair again; second press confirms (see [Relink](#relink)). |
|
|
|
+| Esc | after a first F8 | Cancels the pending relink (the key is consumed). |
|
|
|
| F2 | main screen, any focus | Switch chat list / contact list; selection resets to 0, filter kept. |
|
|
|
-| F3 | main screen, any focus | Toggle autoreply for the open chat. |
|
|
|
+| F3 | main screen | Toggle autoreply for the focused chat: the selected row with list focus, the open chat with input focus. |
|
|
|
| F4 | main screen, any focus | Toggle the global autoreply ("AI tutte"). |
|
|
|
| Tab | main screen | Switch focus between list and input. |
|
|
|
| Up / Down | list focus | Move selection by one row, clamped to the filtered list. |
|
|
|
@@ -168,109 +210,122 @@ Commands use function keys (`src/app.rs:23-32`); `handle_key`
|
|
|
| Esc | input focus | Return focus to the list. |
|
|
|
| Enter | input focus | Send the trimmed input to the open chat. |
|
|
|
|
|
|
-Non-main screens ignore everything except F10. Ctrl chords are 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:1434-1441`) 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 list
|
|
|
-mode with F2 also makes the event loop clear the terminal (see
|
|
|
+F10 and F8 are checked first, on every screen. Any other key cancels a
|
|
|
+pending relink; an Esc that does so is consumed, every other key then
|
|
|
+continues normally. Non-main screens ignore everything else. Ctrl chords are
|
|
|
+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:1592-1594`)
|
|
|
+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
|
|
|
+list mode with F2 also makes the event loop repaint the whole terminal (see
|
|
|
[Runtime and event loop](runtime-and-event-loop.md)).
|
|
|
|
|
|
### List filter
|
|
|
|
|
|
Typing while the list has focus edits `filter` (`handle_list_key`,
|
|
|
-`edit_filter`, `src/app.rs:812-848`). `visible_chats` / `visible_contacts`
|
|
|
-(`src/app.rs:923-933`) keep the entries whose name contains the filter,
|
|
|
-case-insensitively (`matches_filter`, `src/app.rs:1148-1150`); selection,
|
|
|
-opening and deletion all work on the filtered list.
|
|
|
+`edit_filter`, `src/app.rs:937-973`). `visible_chats` / `visible_contacts`
|
|
|
+(`src/app.rs:1048-1058`) keep the entries whose name contains the filter,
|
|
|
+case-insensitively (`matches_filter`, `src/app.rs:1289-1291`); selection,
|
|
|
+opening, F3 and deletion all work on the filtered list.
|
|
|
|
|
|
### Local chat deletion
|
|
|
|
|
|
-`request_delete` (`src/app.rs:867-884`) works only in chat mode: the first
|
|
|
+`request_delete` (`src/app.rs:992-1009`) works only in chat mode: the first
|
|
|
Delete stores the selected chat in `pending_delete` and shows
|
|
|
`Eliminare la chat <name>? Canc conferma, Esc annulla`; a second Delete on
|
|
|
-the same chat calls `confirm_delete` (`src/app.rs:891-906`), which removes
|
|
|
+the same chat calls `confirm_delete` (`src/app.rs:1016-1031`), which removes
|
|
|
the chat from the history (messages, image descriptions and row; no WhatsApp
|
|
|
call), drops its pending reply, kept images and answered snapshot, closes it
|
|
|
-if open and shows `Chat <name> eliminata`. Any other key (Tab, F2, F3, F4,
|
|
|
+if open and shows `Chat <name> eliminata`. Any other key (Tab, F2, F3, F4, F8,
|
|
|
list keys, Ctrl chords) cancels the pending deletion with
|
|
|
-`Eliminazione annullata` (`cancel_pending_delete`, `src/app.rs:854-860`).
|
|
|
+`Eliminazione annullata` (`cancel_pending_delete`, `src/app.rs:979-985`).
|
|
|
|
|
|
### Opening and toggling
|
|
|
|
|
|
-`open_selected` (`src/app.rs:1075-1091`) loads display name, per-chat
|
|
|
-autoreply flag and the last `MESSAGES_SHOWN` (200) messages of the selected
|
|
|
-visible entry, then moves focus to the input. The `reload_*` helpers
|
|
|
-(`src/app.rs:1108-1133`) keep `selected` inside the active filtered list.
|
|
|
-
|
|
|
-`toggle_autoreply` (F3, `src/app.rs:981-986`) delegates to
|
|
|
-`toggle_chat_autoreply` (`src/app.rs:994-1007`), which always allows
|
|
|
-disabling. Enabling goes through `can_enable_autoreply`
|
|
|
-(`src/app.rs:1408-1416`), which first requires an AI client and then a chat
|
|
|
+`selected_jid` (`src/app.rs:1221-1232`) returns the JID on the selected row of
|
|
|
+the active filtered list (chat or contact), or nothing when it is empty.
|
|
|
+`open_selected` (`src/app.rs:1205-1219`) uses it to load display name,
|
|
|
+per-chat autoreply flag and the last `MESSAGES_SHOWN` (200) messages, then
|
|
|
+moves focus to the input. The `reload_*` helpers (`src/app.rs:1249-1274`) keep
|
|
|
+`selected` inside the active filtered list.
|
|
|
+
|
|
|
+`toggle_autoreply` (F3, `src/app.rs:1101-1116`) picks the target from the
|
|
|
+focus — `selected_jid` with list focus, `open_chat` with input focus — and
|
|
|
+delegates to `toggle_chat_autoreply` (`src/app.rs:1124-1137`), which always
|
|
|
+allows disabling and updates `open_chat_autoreply` when the target is the open
|
|
|
+chat. Enabling goes through `can_enable_autoreply`
|
|
|
+(`src/app.rs:1549-1557`), which first requires an AI client and then a chat
|
|
|
accepted by `supports_autoreply` (private chats and groups; broadcasts,
|
|
|
status and newsletters are refused with
|
|
|
`Autoreply disponibile solo nelle chat private e nei gruppi`). The flag is
|
|
|
persisted with `History::set_autoreply` and the status shows
|
|
|
`Autoreply ON|OFF per <name>`.
|
|
|
|
|
|
-`toggle_global_autoreply` (F4, `src/app.rs:1058-1069`) refuses to enable
|
|
|
+`toggle_global_autoreply` (F4, `src/app.rs:1192-1203`) refuses to enable
|
|
|
without an AI client (`STATUS_AI_NOT_CONFIGURED`), persists the flag with
|
|
|
`History::set_global_autoreply`, updates `global_autoreply` and shows
|
|
|
`Autoreply globale ON|OFF`. Disabling is always allowed.
|
|
|
|
|
|
## Web panel requests
|
|
|
|
|
|
-`handle_web_request` (`src/app.rs:1014-1025`) applies a `WebCommand` and then
|
|
|
+`handle_web_request` (`src/app.rs:1139-1156`) applies a `WebCommand` and then
|
|
|
always sends back a `WebState` snapshot on the request's oneshot channel:
|
|
|
|
|
|
- `State` → nothing, just the snapshot;
|
|
|
- `ToggleGlobal` → `toggle_global_autoreply` (same rules as F4);
|
|
|
-- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1032-1038`): if the JID is
|
|
|
+- `ToggleChat(jid)` → `toggle_web_chat` (`src/app.rs:1163-1169`): if the JID is
|
|
|
not among the panel's chats the status becomes `Chat non trovata`,
|
|
|
otherwise `toggle_chat_autoreply` (same rules as F3);
|
|
|
+- `Relink` → `start_relink` (no second confirmation; ignored while a relink runs);
|
|
|
- `Quit` → `should_quit = true`.
|
|
|
|
|
|
-`web_state` (`src/app.rs:1044-1050`) returns the global flag, the status line
|
|
|
-and `web_chats` (`src/app.rs:1422-1432`): the TUI chat list in the same order,
|
|
|
-restricted to chats that support autoreply. See
|
|
|
+`web_state` (`src/app.rs:1171-1184`) returns the global flag, the status line,
|
|
|
+`web_chats` (`src/app.rs:1575-1585`) — the TUI chat list in the same order,
|
|
|
+restricted to chats that support autoreply — the current screen as
|
|
|
+`connecting` / `pairing` / `main` (`web_screen`, `src/app.rs:1563-1569`) 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).
|
|
|
|
|
|
## Sending and background tasks
|
|
|
|
|
|
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
|
|
|
-shutdown (`src/app.rs:1194-1197`).
|
|
|
+shutdown (`src/app.rs:1335-1338`).
|
|
|
|
|
|
-- `submit_input` (`src/app.rs:955-963`) ignores blank input or no open chat,
|
|
|
+- `submit_input` (`src/app.rs:1080-1088`) ignores blank input or no open chat,
|
|
|
clears the input and calls `spawn_send` with `is_ai: false`.
|
|
|
-- `spawn_send` (`src/app.rs:1377-1386`) calls `WaHandle::send_text` and emits
|
|
|
- `Sent`.
|
|
|
-- `handle_sent` (`src/app.rs:741-765`) stores a successful send as a
|
|
|
+- `spawn_send` (`src/app.rs:1518-1527`) calls `WaHandle::send_text` and emits
|
|
|
+ `Sent`; during a relink no session is running, so sends fail with a send
|
|
|
+ error.
|
|
|
+- `handle_sent` (`src/app.rs:858-882`) stores a successful send as a
|
|
|
`from_me` message (with `is_ai` preserved) and reloads the views; a failure
|
|
|
sets `Invio fallito: …`.
|
|
|
-- `spawn_ai_request` (`src/app.rs:1218-1229`) runs the text completion in one
|
|
|
+- `spawn_ai_request` (`src/app.rs:1359-1370`) runs the text completion in one
|
|
|
task and waits for it in another, so `AiReply` is always emitted — even when
|
|
|
the request task panics — and the chat never stays pending.
|
|
|
-- `spawn_image_descriptions` (`src/app.rs:1240-1263`) runs one task per image
|
|
|
+- `spawn_image_descriptions` (`src/app.rs:1381-1404`) runs one task per image
|
|
|
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
|
|
|
`ImagesDescribed`; a failed image does not stop the others and the bytes
|
|
|
stay in memory.
|
|
|
-- `handle_ai_reply` (`src/app.rs:695-701`) shows
|
|
|
+- `handle_ai_reply` (`src/app.rs:812-818`) shows
|
|
|
`AI: nessuna risposta necessaria` for `Reply::NoReply`, `Errore AI: …` for
|
|
|
an error, and passes text to `send_ai_reply_if_enabled`
|
|
|
- (`src/app.rs:709-722`), which sends it only if the chat is still enabled and
|
|
|
+ (`src/app.rs:826-839`), which sends it only if the chat is still enabled and
|
|
|
the owner did not answer by hand meanwhile (`owner_answered_meanwhile`);
|
|
|
otherwise it shows `Risposta AI scartata: autoreply disattivato` or
|
|
|
`Risposta AI scartata: hai già risposto`.
|
|
|
+- The relink task (see [Relink](#relink)) runs `WaHandle::relink` and emits
|
|
|
+ `Relinked`.
|
|
|
|
|
|
## Tests
|
|
|
|
|
|
-Unit tests at `src/app.rs:1443` onward cover `can_enable_autoreply` (private,
|
|
|
+Unit tests at `src/app.rs:1596` onward cover `can_enable_autoreply` (private,
|
|
|
self-chat LID, groups allowed; broadcasts/newsletters and missing AI refused),
|
|
|
`web_chats` filtering and order, `vision_applies`,
|
|
|
`select_images_to_describe` (last five, skip known and out-of-context),
|
|
|
`arrived_after`, `manual_reply_after`, `description_failure_status`,
|
|
|
`matches_filter`, selection clamping/stepping and page steps, the toggle
|
|
|
-helpers, `is_command` and the fixed function-key codes (F2, F3, F4, F10). See
|
|
|
-[Unit tests](../testing/unit-tests.md).
|
|
|
+helpers, `is_command` and the fixed function-key codes (F2, F3, F4, F10; F8 is
|
|
|
+not part of that check). Relink, configuration reload and F3 targeting have no
|
|
|
+`App`-level tests. See [Unit tests](../testing/unit-tests.md).
|