Coverage Report

Created: 2026-10-23 22:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
super-stt-app/src/daemon/client/v1/contexts/mod.rs
Line
Count
Source
1
// SPDX-License-Identifier: GPL-3.0-only
2
//! `/context/...` — the named dictation contexts and which one is in force.
3
//!
4
//! Contract: `docs/protocol/endpoints/v1/context.md`.
5
//!
6
//! A context is what the user is dictating: a `prompt` for a model that follows
7
//! instructions, a `vocabulary` of terms for one that does not. The daemon owns
8
//! the set, one is active at a time, and a backend may be pointed at a different
9
//! one — see [`backend`].
10
//!
11
//! Hand-written rather than generated by the settings macros, for the same
12
//! reason the daemon's side is: those macros are for a path with one value and
13
//! no parameter, and every path here has an id in it.
14
15
pub(crate) mod backend;
16
17
use crate::daemon::client::internal::response::require_unit;
18
use crate::daemon::client::internal::session::with_settings_token;
19
use serde::Deserialize;
20
use super_stt_shared::daemon::http_client::HttpResult;
21
use super_stt_shared::daemon::http_client::transport;
22
use super_stt_shared::models::contexts::DictationContext;
23
24
/// Every context, and which one is in force.
25
#[derive(Debug, Clone, Default, Deserialize)]
26
pub struct ContextCatalog {
27
    /// The id of the context in force, or `None` when none is.
28
    #[serde(default)]
29
    pub active: Option<String>,
30
    /// In the order the user arranged them, which is the order to render.
31
    #[serde(default)]
32
    pub contexts: Vec<DictationContext>,
33
}
34
35
/// One context, as the daemon stored it.
36
#[derive(Debug, Clone, Deserialize)]
37
struct OneContext {
38
    context: DictationContext,
39
}
40
41
/// The active selection, as the daemon reports it after a write.
42
///
43
/// The object is not read here: `GET /context/list` already carries both the
44
/// list and the active id, and the page re-reads that after every write anyway
45
/// — the selection is the one change a *different* row also has to show. This
46
/// type exists so the write's answer is parsed rather than discarded, which is
47
/// what makes a `404` on an unknown id an error instead of a silent no-op.
48
#[derive(Debug, Clone, Default, Deserialize)]
49
pub struct ActiveContext {
50
    #[serde(default)]
51
    id: Option<String>,
52
}
53
54
/// `GET /context/list` — every context plus the active id.
55
0
pub async fn list_contexts() -> HttpResult<ContextCatalog> {
56
0
    with_settings_token(|socket, token| async move {
57
0
        transport::get_json::<ContextCatalog>(socket, &token, "/context/list").await
58
0
    })
59
0
    .await
60
0
}
61
62
/// `POST /context/{id}` — create or replace, answering with the context as
63
/// stored.
64
///
65
/// Reading the answer rather than echoing what was sent is the point: the
66
/// daemon drops blank vocabulary terms and trims the rest, so the version that
67
/// comes back is the one the list should show.
68
0
pub async fn set_context(context: DictationContext) -> HttpResult<DictationContext> {
69
0
    with_settings_token(move |socket, token| {
70
0
        let context = context.clone();
71
0
        async move {
72
0
            let path = format!("/context/{}", urlencoding::encode(&context.id));
73
0
            let body = serde_json::json!({
74
0
                "name": context.name,
75
0
                "prompt": context.prompt,
76
0
                "vocabulary": context.vocabulary,
77
            });
78
0
            transport::post_json::<OneContext>(socket, &token, &path, &body)
79
0
                .await
80
0
                .map(|answer| answer.context)
81
0
        }
82
0
    })
83
0
    .await
84
0
}
85
86
/// `DELETE /context/{id}`.
87
0
pub async fn delete_context(id: String) -> HttpResult<()> {
88
0
    with_settings_token(move |socket, token| {
89
0
        let id = id.clone();
90
0
        async move {
91
0
            let path = format!("/context/{}", urlencoding::encode(&id));
92
0
            let resp = transport::settings_delete(socket, &token, &path).await?;
93
0
            require_unit(resp, "delete_context")
94
0
        }
95
0
    })
96
0
    .await
97
0
}
98
99
/// `POST /context/active` — put a context in force, or (`None`) leave none
100
/// active. Answers with the id now in force.
101
0
pub async fn set_active_context(id: Option<String>) -> HttpResult<Option<String>> {
102
0
    with_settings_token(move |socket, token| {
103
0
        let id = id.clone();
104
0
        async move {
105
0
            let body = serde_json::json!({ "id": id });
106
0
            transport::post_json::<ActiveContext>(socket, &token, "/context/active", &body)
107
0
                .await
108
0
                .map(|answer| answer.id)
109
0
        }
110
0
    })
111
0
    .await
112
0
}