Skip to main content

gpui_kit/agent/
permission.rs

1//! What is currently permitted, across a set of subjects and actions.
2//!
3//! # A cell has four states, not two
4//!
5//! [`PermissionState`] keeps `NotApplicable` apart from `Denied`. "This tool
6//! has no network to reach" and "this tool is refused the network" are
7//! different sentences: the first says the question does not arise, the
8//! second says somebody answered it no. Drawing them the same way would
9//! invent a refusal nobody made, so they are different states, different
10//! wording, and different published values.
11//!
12//! # Where a permission came from
13//!
14//! A permission inherited from a broader rule is not the same as one set in
15//! this matrix, and a reader who cannot tell them apart cannot tell what
16//! changing a cell would actually do. The component therefore *carries and
17//! shows* provenance through [`PermissionSource`], and *derives* none of it:
18//! working out which rule won is policy evaluation over a rule set the host
19//! owns, which is exactly the kind of fact this library takes as an input
20//! rather than computing. Every cell says either where it was inherited from,
21//! in the host's own words, or that it was set here.
22//!
23//! # Read-only and editable
24//!
25//! A matrix with no `on_change` handler is read-only: its cells publish
26//! [`Role::Cell`] and install nothing. A cell whose state is `NotApplicable`
27//! installs nothing either, in an editable matrix as much as a read-only one,
28//! because there is no state for it to cycle to.
29
30use std::rc::Rc;
31
32use gpui::{
33    App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
34    StatefulInteractiveElement, Styled, Window, div, px,
35};
36use gpui_kit_semantics::{NodeSpec, Role, Semantic};
37use gpui_kit_theme::{ActiveTheme, Elevation, Radius, Space, Surface, TextTone, Theme, TypeScale};
38
39use crate::foundation::{FocusRing, Ident, StyledExt, text};
40use crate::strings::{ActiveStrings, StringKey};
41
42type ChangeHandler = Rc<dyn Fn(PermissionChange, &mut Window, &mut App)>;
43
44/// What a cell says about one subject and one action.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum PermissionState {
47    /// Goes ahead without asking.
48    Allowed,
49    /// Refused. Somebody decided this.
50    Denied,
51    /// Permitted only after somebody says so, every time.
52    #[default]
53    Ask,
54    /// The question does not arise here. Never a refusal.
55    NotApplicable,
56}
57
58impl PermissionState {
59    /// The stable name published in the semantic tree.
60    pub fn name(self) -> &'static str {
61        match self {
62            Self::Allowed => "allowed",
63            Self::Denied => "denied",
64            Self::Ask => "ask",
65            Self::NotApplicable => "not-applicable",
66        }
67    }
68
69    /// The wording a reader reads.
70    pub fn label(self, cx: &App) -> SharedString {
71        cx.strings().text(match self {
72            Self::Allowed => StringKey::PermissionAllowed,
73            Self::Denied => StringKey::PermissionDenied,
74            Self::Ask => StringKey::PermissionAsk,
75            Self::NotApplicable => StringKey::PermissionNotApplicable,
76        })
77    }
78
79    /// What operating the cell would ask for next.
80    ///
81    /// `NotApplicable` has no next state: a question that does not arise
82    /// cannot be answered by clicking.
83    pub fn next(self) -> Option<Self> {
84        match self {
85            Self::Allowed => Some(Self::Ask),
86            Self::Ask => Some(Self::Denied),
87            Self::Denied => Some(Self::Allowed),
88            Self::NotApplicable => None,
89        }
90    }
91
92    fn color(self, theme: &Theme) -> gpui::Hsla {
93        match self {
94            Self::Allowed => theme.colors.success,
95            Self::Denied => theme.colors.danger,
96            Self::Ask => theme.colors.warning,
97            Self::NotApplicable => theme.colors.text_faint,
98        }
99    }
100}
101
102/// Where a cell's state was decided.
103#[derive(Debug, Clone, PartialEq, Eq, Default)]
104pub enum PermissionSource {
105    /// Set on this subject, for this action.
106    #[default]
107    Here,
108    /// Inherited from a broader rule, named in the host's own words.
109    Inherited(SharedString),
110}
111
112impl PermissionSource {
113    pub fn inherited(from: impl Into<SharedString>) -> Self {
114        Self::Inherited(from.into())
115    }
116
117    /// The stable name published in the semantic tree.
118    pub fn name(&self) -> &'static str {
119        match self {
120            Self::Here => "here",
121            Self::Inherited(_) => "inherited",
122        }
123    }
124
125    pub fn label(&self, cx: &App) -> SharedString {
126        match self {
127            Self::Here => cx.strings().text(StringKey::PermissionSetHere),
128            Self::Inherited(from) => cx.strings().format(StringKey::PermissionInherited, &[from]),
129        }
130    }
131}
132
133/// One cell: a state, and where it came from.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct PermissionEntry {
136    state: PermissionState,
137    source: PermissionSource,
138}
139
140impl PermissionEntry {
141    /// A state decided in this matrix.
142    pub fn new(state: PermissionState) -> Self {
143        Self {
144            state,
145            source: PermissionSource::Here,
146        }
147    }
148
149    /// A state a broader rule decided, naming that rule.
150    pub fn inherited(state: PermissionState, from: impl Into<SharedString>) -> Self {
151        Self {
152            state,
153            source: PermissionSource::inherited(from),
154        }
155    }
156
157    pub fn state(&self) -> PermissionState {
158        self.state
159    }
160
161    pub fn source(&self) -> &PermissionSource {
162        &self.source
163    }
164}
165
166/// One column: an action, addressed by a stable key.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct PermissionAction {
169    key: SharedString,
170    label: SharedString,
171}
172
173impl PermissionAction {
174    pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
175        Self {
176            key: key.into(),
177            label: label.into(),
178        }
179    }
180
181    pub fn key(&self) -> &SharedString {
182        &self.key
183    }
184}
185
186/// One row: whatever the permissions are about, and its cells.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct PermissionSubject {
189    id: SharedString,
190    label: SharedString,
191    cells: Vec<(SharedString, PermissionEntry)>,
192}
193
194impl PermissionSubject {
195    pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
196        Self {
197            id: id.into(),
198            label: label.into(),
199            cells: Vec::new(),
200        }
201    }
202
203    /// States one cell. An action this subject never states is rendered
204    /// `NotApplicable`, which is the honest reading of a row that does not
205    /// mention it — and is still not a refusal.
206    pub fn cell(mut self, action: impl Into<SharedString>, entry: PermissionEntry) -> Self {
207        self.cells.push((action.into(), entry));
208        self
209    }
210
211    pub fn id(&self) -> &SharedString {
212        &self.id
213    }
214
215    fn entry(&self, action: &SharedString) -> PermissionEntry {
216        self.cells
217            .iter()
218            .find(|(key, _)| key == action)
219            .map(|(_, entry)| entry.clone())
220            .unwrap_or_else(|| PermissionEntry::new(PermissionState::NotApplicable))
221    }
222}
223
224/// What operating a cell asks for. Nothing is applied.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct PermissionChange {
227    pub subject: SharedString,
228    pub action: SharedString,
229    pub next: PermissionState,
230}
231
232/// A grid of subjects against actions.
233#[derive(IntoElement)]
234pub struct PermissionMatrix {
235    ident: Ident,
236    actions: Vec<PermissionAction>,
237    subjects: Vec<PermissionSubject>,
238    on_change: Option<ChangeHandler>,
239}
240
241impl std::fmt::Debug for PermissionMatrix {
242    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        formatter
244            .debug_struct("PermissionMatrix")
245            .field("ident", &self.ident)
246            .field("actions", &self.actions.len())
247            .field("subjects", &self.subjects.len())
248            .field("editable", &self.on_change.is_some())
249            .finish()
250    }
251}
252
253impl PermissionMatrix {
254    pub fn new(ident: impl Into<Ident>) -> Self {
255        Self {
256            ident: ident.into(),
257            actions: Vec::new(),
258            subjects: Vec::new(),
259            on_change: None,
260        }
261    }
262
263    pub fn action(mut self, action: PermissionAction) -> Self {
264        self.actions.push(action);
265        self
266    }
267
268    pub fn actions(mut self, actions: impl IntoIterator<Item = PermissionAction>) -> Self {
269        self.actions.extend(actions);
270        self
271    }
272
273    pub fn subject(mut self, subject: PermissionSubject) -> Self {
274        self.subjects.push(subject);
275        self
276    }
277
278    pub fn subjects(mut self, subjects: impl IntoIterator<Item = PermissionSubject>) -> Self {
279        self.subjects.extend(subjects);
280        self
281    }
282
283    /// Makes the matrix editable. Without a handler it is read-only, and a
284    /// read-only cell installs nothing rather than installing a handler that
285    /// does nothing.
286    pub fn on_change(
287        mut self,
288        handler: impl Fn(PermissionChange, &mut Window, &mut App) + 'static,
289    ) -> Self {
290        self.on_change = Some(Rc::new(handler));
291        self
292    }
293}
294
295impl RenderOnce for PermissionMatrix {
296    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
297        let theme = cx.theme().clone();
298        let heading = div()
299            .row()
300            .w_full()
301            .gap_token(&theme, Space::Sm)
302            .px_token(&theme, Space::Sm)
303            .py_token(&theme, Space::Xs)
304            .child(
305                div().w(px(160.0)).flex_none().child(
306                    text(
307                        &theme,
308                        TypeScale::Caption,
309                        cx.strings().text(StringKey::PermissionSubjectHeading),
310                    )
311                    .text_tone(&theme, TextTone::Faint),
312                ),
313            )
314            .children(self.actions.iter().map(|action| {
315                div().flex_1().min_w_0().child(
316                    text(&theme, TypeScale::Caption, action.label.clone())
317                        .text_tone(&theme, TextTone::Faint),
318                )
319            }));
320
321        let rows: Vec<_> = self
322            .subjects
323            .iter()
324            .map(|subject| {
325                let row_ident = self.ident.child(subject.id.as_ref());
326                div()
327                    .row()
328                    .items_start()
329                    .w_full()
330                    .gap_token(&theme, Space::Sm)
331                    .px_token(&theme, Space::Sm)
332                    .py_token(&theme, Space::Xs)
333                    .child(
334                        div()
335                            .w(px(160.0))
336                            .flex_none()
337                            .child(text(&theme, TypeScale::Label, subject.label.clone()))
338                            .semantic_in(
339                                cx,
340                                NodeSpec::new(row_ident.semantic_id(), Role::Row)
341                                    .text(subject.label.clone())
342                                    .parent(self.ident.semantic_id()),
343                            ),
344                    )
345                    .children(self.actions.iter().map(|action| {
346                        cell(
347                            &theme,
348                            &row_ident,
349                            subject,
350                            action,
351                            self.on_change.clone(),
352                            cx,
353                        )
354                    }))
355            })
356            .collect();
357
358        div()
359            .column()
360            .w_full()
361            .radius(&theme, Radius::Card)
362            .frame(&theme, Surface::Panel, Elevation::Raised)
363            .child(heading)
364            .children(rows)
365            .semantic_in(
366                cx,
367                NodeSpec::new(self.ident.semantic_id(), Role::Table)
368                    .value(SharedString::from(self.subjects.len().to_string())),
369            )
370    }
371}
372
373fn cell(
374    theme: &Theme,
375    row_ident: &Ident,
376    subject: &PermissionSubject,
377    action: &PermissionAction,
378    on_change: Option<ChangeHandler>,
379    cx: &App,
380) -> gpui::AnyElement {
381    let entry = subject.entry(&action.key);
382    let state = entry.state();
383    let ident = row_ident.child(action.key.as_ref());
384    let name = cx.strings().format(
385        StringKey::PermissionCellName,
386        &[&action.label, &state.label(cx)],
387    );
388    let next = state.next();
389    let handler = on_change.zip(next);
390
391    let mark = div()
392        .row()
393        .gap_token(theme, Space::Xs)
394        .child(
395            div()
396                .flex_none()
397                .size(px(7.0))
398                .rounded_full()
399                .bg(state.color(theme)),
400        )
401        .child(text(theme, TypeScale::Label, state.label(cx)));
402
403    // Provenance is shown on every cell that has a state, so "set here" is a
404    // statement rather than the absence of one.
405    let source = (state != PermissionState::NotApplicable).then(|| {
406        text(theme, TypeScale::Caption, entry.source().label(cx))
407            .text_tone(theme, TextTone::Faint)
408            .semantic_in(
409                cx,
410                NodeSpec::new(ident.child("source").semantic_id(), Role::Text)
411                    .text(entry.source().label(cx))
412                    .value(SharedString::new_static(entry.source().name()))
413                    .parent(ident.semantic_id()),
414            )
415    });
416
417    let body = div()
418        .column()
419        .gap_token(theme, Space::Xs)
420        .child(mark)
421        .children(source);
422
423    let spec = NodeSpec::new(
424        ident.semantic_id(),
425        if handler.is_some() {
426            Role::Button
427        } else {
428            Role::Cell
429        },
430    )
431    .text(name)
432    .value(SharedString::new_static(state.name()))
433    .parent(row_ident.semantic_id());
434
435    // Both arms carry a border so the two matrices line up; only the editable
436    // one is drawn. Hover and the focus ring arrive too late to answer "can I
437    // change this?", and for a permission that question has to be answerable
438    // at rest.
439    let frame = div()
440        .flex_1()
441        .min_w_0()
442        .px_token(theme, Space::Sm)
443        .py_token(theme, Space::Xs)
444        .hairline(theme)
445        .radius(theme, Radius::Control);
446
447    match handler {
448        Some((handler, next)) => {
449            let subject_id = subject.id.clone();
450            let action_key = action.key.clone();
451            frame
452                .id(ident.element_id())
453                .tab_index(0)
454                .cursor_pointer()
455                .hover(|style| style.bg(theme.colors.hover))
456                .focus_ring(theme)
457                .on_click(move |_event, window, cx| {
458                    handler(
459                        PermissionChange {
460                            subject: subject_id.clone(),
461                            action: action_key.clone(),
462                            next,
463                        },
464                        window,
465                        cx,
466                    );
467                })
468                .child(body)
469                .semantic_in(cx, spec)
470                .into_any_element()
471        }
472        None => frame
473            .border_color(gpui::transparent_black())
474            .child(body)
475            .semantic_in(cx, spec)
476            .into_any_element(),
477    }
478}