Make the attention count real, and make it carry a source
The sidebar had said ATTENTION · 1 since the first commit. It was reading a fixture's needs_input flag that no live pane ever set, so the number was frozen at whatever the demo data said. Attention is now built from observed signals, and every signal carries an AttentionSource. The rule, enforced by is_countable on the source rather than by a filter at the call site: a guess may draw a row and sort it, but may not increment the count. That is decision 0012's provenance rule applied to a different claim, for the same reason — a wrong count teaches people to ignore the number, and the number is the whole point of the section. RuntimeObserved is the only source that produces signals today, from process exits and runtime faults. The two ACP kinds are declared and never constructed, so there is a shape for the ACP client to fill and nobody is tempted to approximate "asked you a question" by watching output for a question mark. RuntimeEvent::Exited carries a u32 code that was being formatted into a sentence and discarded; AttentionKind::Finished keeps it, which is why a row can say "Exited with code 42" instead of "needs attention". Verified live: exiting a pane with code 42 produces ATTENTION · 1, a row reading "Exited with code 42 · observed", a dimmed pane banner offering restart, and a live-PTY count that drops from 3/3 to 2/3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d76da3babb
commit
3a8a100ea5
@@ -0,0 +1,241 @@
|
|||||||
|
//! What is waiting on a human, and how we know.
|
||||||
|
//!
|
||||||
|
//! The usage footer already refuses to show a number without saying where it
|
||||||
|
//! came from. Attention is the same kind of claim and gets the same treatment:
|
||||||
|
//! "three panes need you" is worth acting on if three panes actually asked, and
|
||||||
|
//! worth nothing if it was guessed from window titles.
|
||||||
|
//!
|
||||||
|
//! So every signal carries a [`AttentionSource`], and the rule — enforced by a
|
||||||
|
//! test, not by care — is that a guessed signal may draw an indicator and sort a
|
||||||
|
//! row, but **may not increment the count**. A number in the sidebar is a
|
||||||
|
//! promise that something is really waiting.
|
||||||
|
|
||||||
|
/// Why a pane wants a human.
|
||||||
|
///
|
||||||
|
/// The two ACP variants are deliberately unreachable: a harness asking for
|
||||||
|
/// permission or asking a question arrives over the ACP client, which does not
|
||||||
|
/// exist yet. They are declared rather than faked, so the day the client lands
|
||||||
|
/// there is a defined shape for it to fill, and so nothing invents them from a
|
||||||
|
/// pattern in the output in the meantime.
|
||||||
|
#[allow(dead_code, reason = "the ACP variants land with the ACP client")]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum AttentionKind {
|
||||||
|
/// The runtime broke. Nothing will happen until someone looks.
|
||||||
|
Faulted,
|
||||||
|
/// The process ended. Whether that is finished or failed is in the code.
|
||||||
|
Finished { exit_code: u32 },
|
||||||
|
/// A harness asked for permission to act. Needs the ACP client.
|
||||||
|
PermissionRequested,
|
||||||
|
/// A harness asked the user a question. Needs the ACP client.
|
||||||
|
QuestionAsked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttentionKind {
|
||||||
|
/// The words the sidebar shows. Present tense, specific, no jargon.
|
||||||
|
pub(crate) fn label(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Faulted => "Runtime faulted".to_owned(),
|
||||||
|
Self::Finished { exit_code: 0 } => "Finished".to_owned(),
|
||||||
|
Self::Finished { exit_code } => format!("Exited with code {exit_code}"),
|
||||||
|
Self::PermissionRequested => "Waiting for permission".to_owned(),
|
||||||
|
Self::QuestionAsked => "Asked you a question".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How we came to believe it.
|
||||||
|
///
|
||||||
|
/// Ordered from most to least trustworthy, which is also the order the sidebar
|
||||||
|
/// sorts by when two panes want attention at once.
|
||||||
|
#[allow(
|
||||||
|
dead_code,
|
||||||
|
reason = "ACP and heuristic sources land with the clients that produce them"
|
||||||
|
)]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub(crate) enum AttentionSource {
|
||||||
|
/// The harness asked, over ACP. It said so itself.
|
||||||
|
AcpRequest,
|
||||||
|
/// The harness reported it through a documented status surface.
|
||||||
|
HarnessReported,
|
||||||
|
/// Lumbridge observed it in a process it supervises: an exit, a fault.
|
||||||
|
RuntimeObserved,
|
||||||
|
/// Inferred from a window title or output pattern. A guess.
|
||||||
|
TitleHeuristic,
|
||||||
|
/// Test or demonstration data.
|
||||||
|
Fixture,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttentionSource {
|
||||||
|
/// Whether a signal from this source may be counted.
|
||||||
|
///
|
||||||
|
/// A guess can be shown and can order a list. It cannot make the sidebar
|
||||||
|
/// assert that something is waiting, because a wrong count is worse than no
|
||||||
|
/// count: it teaches people to ignore the number.
|
||||||
|
pub(crate) const fn is_countable(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::AcpRequest | Self::HarnessReported | Self::RuntimeObserved
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::AcpRequest => "asked",
|
||||||
|
Self::HarnessReported => "reported",
|
||||||
|
Self::RuntimeObserved => "observed",
|
||||||
|
Self::TitleHeuristic => "inferred",
|
||||||
|
Self::Fixture => "fixture",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One pane wanting a human.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct AttentionSignal<Pane> {
|
||||||
|
pub(crate) pane: Pane,
|
||||||
|
pub(crate) kind: AttentionKind,
|
||||||
|
pub(crate) source: AttentionSource,
|
||||||
|
pub(crate) observed_at_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Pane> AttentionSignal<Pane> {
|
||||||
|
pub(crate) const fn is_countable(&self) -> bool {
|
||||||
|
self.source.is_countable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every pane wanting a human, and how many of those are certain.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct Attention<Pane> {
|
||||||
|
signals: Vec<AttentionSignal<Pane>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Pane: Copy + Eq> Attention<Pane> {
|
||||||
|
pub(crate) const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
signals: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records a signal, replacing any earlier one for the same pane.
|
||||||
|
///
|
||||||
|
/// One pane wants attention for one reason at a time; keeping a history
|
||||||
|
/// would mean the count grew every time a pane was restarted.
|
||||||
|
pub(crate) fn observe(&mut self, signal: AttentionSignal<Pane>) {
|
||||||
|
self.signals.retain(|existing| existing.pane != signal.pane);
|
||||||
|
self.signals.push(signal);
|
||||||
|
// Most trustworthy first, then oldest first: the thing that has been
|
||||||
|
// waiting longest is the thing to deal with.
|
||||||
|
self.signals
|
||||||
|
.sort_by_key(|signal| (signal.source, signal.observed_at_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forgets a pane's signal, because it is no longer waiting.
|
||||||
|
pub(crate) fn clear(&mut self, pane: Pane) {
|
||||||
|
self.signals.retain(|signal| signal.pane != pane);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn signals(&self) -> &[AttentionSignal<Pane>] {
|
||||||
|
&self.signals
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many panes are *certainly* waiting.
|
||||||
|
///
|
||||||
|
/// This is the number the sidebar shows. Guessed signals are excluded by
|
||||||
|
/// construction rather than by remembering to filter them.
|
||||||
|
pub(crate) fn countable(&self) -> usize {
|
||||||
|
self.signals
|
||||||
|
.iter()
|
||||||
|
.filter(|signal| signal.is_countable())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Attention, AttentionKind, AttentionSignal, AttentionSource};
|
||||||
|
|
||||||
|
fn signal(pane: u8, source: AttentionSource, at: u64) -> AttentionSignal<u8> {
|
||||||
|
AttentionSignal {
|
||||||
|
pane,
|
||||||
|
kind: AttentionKind::Faulted,
|
||||||
|
source,
|
||||||
|
observed_at_ms: at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rule this module exists to enforce.
|
||||||
|
#[test]
|
||||||
|
fn a_guess_may_be_shown_but_never_counted() {
|
||||||
|
let mut attention = Attention::new();
|
||||||
|
attention.observe(signal(1, AttentionSource::TitleHeuristic, 10));
|
||||||
|
attention.observe(signal(2, AttentionSource::Fixture, 20));
|
||||||
|
assert_eq!(attention.signals().len(), 2, "both are shown");
|
||||||
|
assert_eq!(
|
||||||
|
attention.countable(),
|
||||||
|
0,
|
||||||
|
"neither may claim that something is waiting"
|
||||||
|
);
|
||||||
|
|
||||||
|
attention.observe(signal(3, AttentionSource::RuntimeObserved, 30));
|
||||||
|
assert_eq!(attention.countable(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_source_declares_whether_it_counts() {
|
||||||
|
for source in [
|
||||||
|
AttentionSource::AcpRequest,
|
||||||
|
AttentionSource::HarnessReported,
|
||||||
|
AttentionSource::RuntimeObserved,
|
||||||
|
] {
|
||||||
|
assert!(source.is_countable(), "{source:?} is a report, not a guess");
|
||||||
|
}
|
||||||
|
for source in [AttentionSource::TitleHeuristic, AttentionSource::Fixture] {
|
||||||
|
assert!(!source.is_countable(), "{source:?} is a guess");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_pane_waits_for_one_reason() {
|
||||||
|
let mut attention = Attention::new();
|
||||||
|
attention.observe(signal(1, AttentionSource::RuntimeObserved, 10));
|
||||||
|
attention.observe(signal(1, AttentionSource::RuntimeObserved, 20));
|
||||||
|
assert_eq!(
|
||||||
|
attention.signals().len(),
|
||||||
|
1,
|
||||||
|
"restarting must not accumulate"
|
||||||
|
);
|
||||||
|
assert_eq!(attention.signals()[0].observed_at_ms, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_most_trustworthy_and_longest_waiting_sorts_first() {
|
||||||
|
let mut attention = Attention::new();
|
||||||
|
attention.observe(signal(1, AttentionSource::TitleHeuristic, 10));
|
||||||
|
attention.observe(signal(2, AttentionSource::RuntimeObserved, 30));
|
||||||
|
attention.observe(signal(3, AttentionSource::AcpRequest, 40));
|
||||||
|
attention.observe(signal(4, AttentionSource::AcpRequest, 20));
|
||||||
|
let order: Vec<u8> = attention.signals().iter().map(|s| s.pane).collect();
|
||||||
|
assert_eq!(order, vec![4, 3, 2, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clearing_a_pane_removes_only_that_pane() {
|
||||||
|
let mut attention = Attention::new();
|
||||||
|
attention.observe(signal(1, AttentionSource::RuntimeObserved, 10));
|
||||||
|
attention.observe(signal(2, AttentionSource::RuntimeObserved, 20));
|
||||||
|
attention.clear(1);
|
||||||
|
assert_eq!(attention.signals().len(), 1);
|
||||||
|
assert_eq!(attention.signals()[0].pane, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kinds_read_as_sentences_and_name_a_failing_code() {
|
||||||
|
assert_eq!(AttentionKind::Finished { exit_code: 0 }.label(), "Finished");
|
||||||
|
assert_eq!(
|
||||||
|
AttentionKind::Finished { exit_code: 130 }.label(),
|
||||||
|
"Exited with code 130"
|
||||||
|
);
|
||||||
|
assert_eq!(AttentionKind::Faulted.label(), "Runtime faulted");
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
-17
@@ -1,3 +1,4 @@
|
|||||||
|
mod attention;
|
||||||
mod keymap;
|
mod keymap;
|
||||||
mod panel_registry;
|
mod panel_registry;
|
||||||
mod theme;
|
mod theme;
|
||||||
@@ -27,6 +28,8 @@ use lumbridge_ui_fixture::{
|
|||||||
SurfaceKind,
|
SurfaceKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use attention::{Attention, AttentionKind, AttentionSignal, AttentionSource};
|
||||||
|
use lumbridge_harness::MonotonicWallClock;
|
||||||
use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane};
|
use panel_registry::{PanelId, PanelKind, PanelRegistry, SeedPane};
|
||||||
use theme::{ActiveTheme, ThemeColors};
|
use theme::{ActiveTheme, ThemeColors};
|
||||||
use usage_feed::{UsageFeed, UsageSegment};
|
use usage_feed::{UsageFeed, UsageSegment};
|
||||||
@@ -95,6 +98,11 @@ struct LumbridgeShell {
|
|||||||
pending_terminate: Option<PanelId>,
|
pending_terminate: Option<PanelId>,
|
||||||
/// Why the last keystroke went nowhere, if it did.
|
/// Why the last keystroke went nowhere, if it did.
|
||||||
input_gap: Option<String>,
|
input_gap: Option<String>,
|
||||||
|
/// What is waiting on a human, and how we know.
|
||||||
|
attention: Attention<PanelId>,
|
||||||
|
/// Stamps attention signals. Monotonic, so a signal cannot appear to have
|
||||||
|
/// arrived before one recorded earlier.
|
||||||
|
clock: MonotonicWallClock,
|
||||||
root_focus: FocusHandle,
|
root_focus: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,6 +720,8 @@ impl LumbridgeShell {
|
|||||||
add_panel_chooser_open: false,
|
add_panel_chooser_open: false,
|
||||||
pending_terminate: None,
|
pending_terminate: None,
|
||||||
input_gap: None,
|
input_gap: None,
|
||||||
|
attention: Attention::new(),
|
||||||
|
clock: MonotonicWallClock::start(),
|
||||||
root_focus,
|
root_focus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -827,6 +837,17 @@ impl LumbridgeShell {
|
|||||||
status.code
|
status.code
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// Observed in a process Lumbridge supervises, so it
|
||||||
|
// counts. The exit code is carried rather than being
|
||||||
|
// formatted into a sentence and thrown away.
|
||||||
|
self.attention.observe(AttentionSignal {
|
||||||
|
pane,
|
||||||
|
kind: AttentionKind::Finished {
|
||||||
|
exit_code: status.code,
|
||||||
|
},
|
||||||
|
source: AttentionSource::RuntimeObserved,
|
||||||
|
observed_at_ms: self.clock.now_ms(),
|
||||||
|
});
|
||||||
changed = true;
|
changed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -837,6 +858,12 @@ impl LumbridgeShell {
|
|||||||
terminal.status =
|
terminal.status =
|
||||||
LiveRuntimeStatus::Fault(format!("{operation:?}: {message}"));
|
LiveRuntimeStatus::Fault(format!("{operation:?}: {message}"));
|
||||||
}
|
}
|
||||||
|
self.attention.observe(AttentionSignal {
|
||||||
|
pane,
|
||||||
|
kind: AttentionKind::Faulted,
|
||||||
|
source: AttentionSource::RuntimeObserved,
|
||||||
|
observed_at_ms: self.clock.now_ms(),
|
||||||
|
});
|
||||||
changed = true;
|
changed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1050,6 +1077,7 @@ impl LumbridgeShell {
|
|||||||
terminal.status = LiveRuntimeStatus::Fault(error.to_string());
|
terminal.status = LiveRuntimeStatus::Fault(error.to_string());
|
||||||
}
|
}
|
||||||
self.live_terminals.insert(pane, terminal);
|
self.live_terminals.insert(pane, terminal);
|
||||||
|
self.attention.clear(pane);
|
||||||
self.pending_terminate = None;
|
self.pending_terminate = None;
|
||||||
window.focus(&self.root_focus);
|
window.focus(&self.root_focus);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -2380,12 +2408,25 @@ impl LumbridgeShell {
|
|||||||
/// The count is derived rather than declared: a sidebar that claims one
|
/// The count is derived rather than declared: a sidebar that claims one
|
||||||
/// pane needs input while no pane is blocked is the same class of untruth
|
/// pane needs input while no pane is blocked is the same class of untruth
|
||||||
/// as an invented usage number.
|
/// as an invented usage number.
|
||||||
fn attention_panels(&self) -> Vec<PanelView> {
|
/// Panes waiting on a human, most trustworthy first.
|
||||||
self.panels
|
///
|
||||||
.attached_ids()
|
/// Read from observed signals rather than from the fixture's
|
||||||
.into_iter()
|
/// `needs_input` flag, which no live pane ever set — the count was frozen
|
||||||
.map(|id| self.panel_view(id))
|
/// at whatever the demo data said.
|
||||||
.filter(PanelView::needs_input)
|
fn attention_rows(&self) -> Vec<AttentionRow> {
|
||||||
|
self.attention
|
||||||
|
.signals()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|signal| {
|
||||||
|
let panel = self.panels.panel(signal.pane)?;
|
||||||
|
Some(AttentionRow {
|
||||||
|
id: signal.pane,
|
||||||
|
title: panel.title.clone(),
|
||||||
|
reason: signal.kind.label(),
|
||||||
|
source: signal.source.label(),
|
||||||
|
countable: signal.is_countable(),
|
||||||
|
})
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2650,11 +2691,12 @@ impl LumbridgeShell {
|
|||||||
)]
|
)]
|
||||||
fn render_sidebar(
|
fn render_sidebar(
|
||||||
&self,
|
&self,
|
||||||
attention_panels: &[PanelView],
|
attention_rows: &[AttentionRow],
|
||||||
detached_entries: &[(PanelId, String)],
|
detached_entries: &[(PanelId, String)],
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
let theme = self.theme.colors;
|
let theme = self.theme.colors;
|
||||||
|
let waiting = self.attention.countable();
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -2669,14 +2711,17 @@ impl LumbridgeShell {
|
|||||||
.pt_4()
|
.pt_4()
|
||||||
.pb_2()
|
.pb_2()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(if attention_panels.is_empty() {
|
.text_color(if waiting == 0 {
|
||||||
theme.muted
|
theme.muted
|
||||||
} else {
|
} else {
|
||||||
theme.attention
|
theme.attention
|
||||||
})
|
})
|
||||||
.child(format!("ATTENTION · {}", attention_panels.len())),
|
// The count is only what is certainly waiting. A guessed
|
||||||
|
// signal still gets a row below, but it may not make the
|
||||||
|
// sidebar assert that something needs you.
|
||||||
|
.child(format!("ATTENTION · {waiting}")),
|
||||||
)
|
)
|
||||||
.when(attention_panels.is_empty(), |view| {
|
.when(attention_rows.is_empty(), |view| {
|
||||||
view.child(
|
view.child(
|
||||||
div()
|
div()
|
||||||
.mx_2()
|
.mx_2()
|
||||||
@@ -2688,8 +2733,8 @@ impl LumbridgeShell {
|
|||||||
.child("No pane is waiting on you"),
|
.child("No pane is waiting on you"),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.children(attention_panels.iter().map(|pane| {
|
.children(attention_rows.iter().map(|row| {
|
||||||
let id = pane.id;
|
let id = row.id;
|
||||||
div()
|
div()
|
||||||
.id(("attention-panel", id.get()))
|
.id(("attention-panel", id.get()))
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
@@ -2700,20 +2745,26 @@ impl LumbridgeShell {
|
|||||||
.rounded(px(5.0))
|
.rounded(px(5.0))
|
||||||
.bg(theme.surface_active)
|
.bg(theme.surface_active)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(theme.attention)
|
// A guess is outlined quietly; a report is outlined in the
|
||||||
|
// attention colour. The row looks like what it is.
|
||||||
|
.border_color(if row.countable {
|
||||||
|
theme.attention
|
||||||
|
} else {
|
||||||
|
theme.border
|
||||||
|
})
|
||||||
.hover(|view| view.bg(theme.surface_raised))
|
.hover(|view| view.bg(theme.surface_raised))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(theme.text)
|
.text_color(theme.text)
|
||||||
.child(pane.title.clone()),
|
.child(row.title.clone()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.mt_1()
|
.mt_1()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(theme.muted)
|
.text_color(theme.muted)
|
||||||
.child(pane.target.clone()),
|
.child(format!("{} · {}", row.reason, row.source)),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
shell.select_pane(id, window, cx);
|
shell.select_pane(id, window, cx);
|
||||||
@@ -2950,6 +3001,17 @@ struct RootParts {
|
|||||||
running_runtime_count: usize,
|
running_runtime_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One pane waiting on a human, as the sidebar shows it.
|
||||||
|
struct AttentionRow {
|
||||||
|
id: PanelId,
|
||||||
|
title: String,
|
||||||
|
/// What it wants, in words.
|
||||||
|
reason: String,
|
||||||
|
/// How we know. Shown so a guess never reads like a report.
|
||||||
|
source: &'static str,
|
||||||
|
countable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// One host or endpoint row in the sidebar's runtime list.
|
/// One host or endpoint row in the sidebar's runtime list.
|
||||||
struct RuntimeRow {
|
struct RuntimeRow {
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
@@ -3015,8 +3077,8 @@ impl Render for LumbridgeShell {
|
|||||||
"{running_runtime_count}/{} LIVE PTYS",
|
"{running_runtime_count}/{} LIVE PTYS",
|
||||||
self.live_terminals.len()
|
self.live_terminals.len()
|
||||||
);
|
);
|
||||||
let attention_panels = self.attention_panels();
|
let attention_rows = self.attention_rows();
|
||||||
let sidebar = self.render_sidebar(&attention_panels, &detached_entries, cx);
|
let sidebar = self.render_sidebar(&attention_rows, &detached_entries, cx);
|
||||||
|
|
||||||
let selected_position = attached_panes
|
let selected_position = attached_panes
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 0019: Attention carries a source, and a guess may not be counted
|
||||||
|
|
||||||
|
Status: accepted; runtime-observed signals are live.
|
||||||
|
|
||||||
|
The sidebar said `ATTENTION · 1` from the first commit. It was reading a
|
||||||
|
fixture's `needs_input` flag that no live pane ever set, so the number was
|
||||||
|
frozen at whatever the demo data said and had no relationship to anything
|
||||||
|
running.
|
||||||
|
|
||||||
|
Replacing it raises the question the usage ledger already answered once: what
|
||||||
|
is a number in this interface allowed to claim?
|
||||||
|
|
||||||
|
## The rule
|
||||||
|
|
||||||
|
Every attention signal carries an `AttentionSource`, and a source that is a
|
||||||
|
guess **may draw a row and may sort it, but may not increment the count**.
|
||||||
|
|
||||||
|
- `AcpRequest` — the harness asked, over ACP. It said so itself.
|
||||||
|
- `HarnessReported` — a documented status surface reported it.
|
||||||
|
- `RuntimeObserved` — Lumbridge saw it in a process it supervises: an exit, a
|
||||||
|
fault. The only source that produces signals today.
|
||||||
|
- `TitleHeuristic` — inferred from a window title or an output pattern.
|
||||||
|
- `Fixture` — test or demonstration data.
|
||||||
|
|
||||||
|
The first three count. The last two do not, and `is_countable` is a method on
|
||||||
|
the source rather than a filter at the call site, so a new signal cannot be
|
||||||
|
counted by forgetting to exclude it.
|
||||||
|
|
||||||
|
This is decision 0012's provenance rule applied to a different claim. The
|
||||||
|
reasoning is the same: a wrong count is worse than no count, because it teaches
|
||||||
|
people to ignore the number, and the number is the whole point of the section.
|
||||||
|
|
||||||
|
## What is deliberately unreachable
|
||||||
|
|
||||||
|
`PermissionRequested` and `QuestionAsked` are declared and never constructed.
|
||||||
|
Those arrive over the ACP client, which does not exist yet.
|
||||||
|
|
||||||
|
They are written down rather than left out so there is a defined shape for the
|
||||||
|
client to fill — and, more importantly, so nobody is tempted to approximate them
|
||||||
|
with a `TitleHeuristic` that watches for a question mark in the output. A pane
|
||||||
|
that *looks* like it is asking something is not a pane that asked. That signal
|
||||||
|
would be a guess, would therefore not be countable, and the honest version of it
|
||||||
|
is simply not to ship it.
|
||||||
|
|
||||||
|
## What a signal replaces
|
||||||
|
|
||||||
|
One pane wants attention for one reason at a time: a new signal replaces the
|
||||||
|
pane's previous one rather than accumulating, or the count would grow every time
|
||||||
|
a pane was restarted. Signals sort by source first and age second, so the most
|
||||||
|
trustworthy and longest-waiting thing is at the top.
|
||||||
|
|
||||||
|
Restarting or terminating a pane clears its signal, because the thing that was
|
||||||
|
waiting is no longer waiting.
|
||||||
|
|
||||||
|
## The exit code survives
|
||||||
|
|
||||||
|
`RuntimeEvent::Exited` carries a `u32` code that used to be formatted straight
|
||||||
|
into a sentence and discarded. `AttentionKind::Finished { exit_code }` keeps it,
|
||||||
|
which is what lets the row say "Exited with code 42" rather than "needs
|
||||||
|
attention", and what will later let a finished-successfully signal be told apart
|
||||||
|
from a failure without re-parsing English.
|
||||||
Reference in New Issue
Block a user