diff --git a/Cargo.lock b/Cargo.lock index 0d8354cf..de99beb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4885,20 +4885,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inspectors" -version = "0.2.2" -dependencies = [ - "gpui", - "graph_client", - "graph_store", - "serde_json", - "studio_actions", - "studio_ui", - "studio_workspace", - "ui", -] - [[package]] name = "interpolate_name" version = "0.2.4" @@ -9722,7 +9708,6 @@ dependencies = [ "graph_store", "history", "image", - "inspectors", "libc", "log", "menu", diff --git a/Cargo.toml b/Cargo.toml index abaf70cb..8cf3c1d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ members = [ "crates/query_editor", "crates/review", "crates/history", - "crates/inspectors", "crates/agent_panel", "crates/studio_update", "crates/oauth_client", @@ -65,7 +64,6 @@ graph_canvas = { path = "crates/graph_canvas" } query_editor = { path = "crates/query_editor" } review = { path = "crates/review" } history = { path = "crates/history" } -inspectors = { path = "crates/inspectors" } agent_panel = { path = "crates/agent_panel" } # Zed, one release tag for every crate (docs/zed-pin.md). GPL-3.0-or-later and Apache-2.0. diff --git a/README.md b/README.md index 3c4e51bc..bd4e534e 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ seconds, to run day to day); `script/bundle-linux` builds a tarball with a `.des the graph through Studio; the agent writes only to a branch Studio creates for it, and never receives the token. - **Shell.** Command palette (⌘K), go to type or node (⌘P), split panes, several windows - restored on launch, the schema's `.pg` source, a storage inspector, and `omnigraph-studio://` + restored on launch, the schema's `.pg` source, and `omnigraph-studio://` links that open a graph, node or query without running anything. `?` lists every binding. - **Settings and shortcuts.** A settings tab (⌘,) generated from the settings schema, with per-graph values. `settings.json` and `keymap.json` (Zed's keymap format, over Studio's @@ -165,7 +165,7 @@ depend on each other. | Layer | Crates | |---|---| | 6 | `studio`: the binary, menus, windows, deep links | -| 5 | `type_table`, `relationship_tree`, `graph_canvas`, `query_editor`, `review`, `history`, `inspectors`, `agent_panel`, `command_palette` | +| 5 | `type_table`, `relationship_tree`, `graph_canvas`, `query_editor`, `review`, `history`, `agent_panel`, `command_palette` | | 4 | `studio_workspace`: panes, tabs, docks, modals, toasts | | 3 | `graph_store`: connections, graph sessions, snapshot pins, exploration | | 2 | `graph_client` (HTTP, no GPUI), `gq_language` (compiler wrapper, no GPUI), `studio_ui` | diff --git a/crates/graph_store/src/graph_store.rs b/crates/graph_store/src/graph_store.rs index 5f31ac52..95bde0b4 100644 --- a/crates/graph_store/src/graph_store.rs +++ b/crates/graph_store/src/graph_store.rs @@ -62,8 +62,8 @@ pub mod cli { } pub use session::{ BranchStart, DraftEntry, DraftKey, ExternalWrite, FollowUp, GraphSession, HeadWatch, HeldAs, - Loadable, MergeEntry, MergeSent, MergeSettled, OutcomeEntry, OutcomeKind, ServerState, - SessionEvent, nothing_sent, open_session, open_session_behind, + Loadable, MergeEntry, MergeSent, MergeSettled, OutcomeEntry, OutcomeKind, SessionEvent, + nothing_sent, open_session, open_session_behind, }; /// The user agent of Studio's requests. diff --git a/crates/graph_store/src/session.rs b/crates/graph_store/src/session.rs index 75f1c411..e798efe4 100644 --- a/crates/graph_store/src/session.rs +++ b/crates/graph_store/src/session.rs @@ -17,12 +17,12 @@ use gq_language::{BuiltQuery, RelationshipGroup, SchemaModel, builder}; use graph_client::api::{ AccessAction, BranchCreateOutput, BranchCreateRequest, BranchDeleteOutput, BranchMergeOutput, BranchMergeRequest, ChangeFeedOutput, ChangeOutput, ChangeRequest, CommitKind, CommitOutput, - DiffOutput, EntityKind, GraphListResponse, InvokeStoredQueryRequest, MergeBaseOutput, - QueryCatalogEntry, QueryRequest, ReadOutput, ReadTargetOutput, ReadinessOutput, SnapshotOutput, + DiffOutput, EntityKind, InvokeStoredQueryRequest, MergeBaseOutput, QueryCatalogEntry, + QueryRequest, ReadOutput, ReadTargetOutput, SnapshotOutput, }; use graph_client::{ ApiErrorSummary, BlobAt, BlobCell, BlobMeta, CommitChanges, FeedAt, GraphClient, ReadError, - ServerHealth, Unauthorized, WriteOutcome, + Unauthorized, WriteOutcome, }; use serde_json::Value; @@ -330,15 +330,6 @@ impl BranchStart { } } -/// The server as the storage inspector shows it ([`GraphSession::server_state`]): each read's -/// own answer, so one failing leaves the others. -pub struct ServerState { - pub health: Result, - pub readiness: Result, - pub graphs: Result, - pub snapshot: Result, -} - /// A read at a commit: the commit, the query text and its parameters, and the schema epoch it /// was read under ([`GraphSession::schema_epoch`]). #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -2603,14 +2594,6 @@ impl GraphSession { self.counts_stale.as_deref() } - /// The storage snapshot the counts were read from, when it is `branch`'s. - pub fn snapshot_of(&self, branch: &str) -> Option> { - self.snapshot - .loaded() - .filter(|snapshot| snapshot.graph_branch == branch) - .cloned() - } - /// Reads `branch`'s head now, and resolves once the session holds what it answered, or no /// read could be sent: a Refresh reads at the newest head, which the next poll would only /// find later, and a view pinned at it still compares the heads after it. @@ -3554,41 +3537,6 @@ impl GraphSession { ) } - /// The server's health and readiness, the graphs it serves, and `branch`'s snapshot: what - /// the storage inspector shows, read together. - pub fn server_state(&mut self, branch: &str, cx: &mut Context) -> Task { - let client = self.client(cx); - // Each is a read of its own, counted and noted as one. - let health = { - let client = client.clone(); - self.counted_read(async move { client.health().await }, cx) - }; - let readiness = { - let client = client.clone(); - self.counted_read(async move { client.readiness().await }, cx) - }; - let graphs = self.counted_read(async move { client.graphs().await }, cx); - let snapshot = self.branch_snapshot(branch, cx); - cx.background_spawn(async move { - ServerState { - health: health.await, - readiness: readiness.await, - graphs: graphs.await, - snapshot: snapshot.await, - } - }) - } - - /// `branch`'s storage snapshot: its tables, versions and row counts. - pub fn branch_snapshot( - &mut self, - branch: &str, - cx: &mut Context, - ) -> Task> { - let (client, graph, branch) = (self.client(cx), self.graph_id.clone(), branch.to_owned()); - self.counted_read(async move { client.snapshot(&graph, &branch).await }, cx) - } - /// How many neighbours `id` has through `group`, at `snapshot` (the branch head when /// `None`), with the commit the count holds at: the one asked for, or the one the read /// answered at, for a count asked for at the head. A view pinned by its first answer @@ -6851,7 +6799,7 @@ mod tests { assert_eq!((reads_of("main"), reads_of("feature")), (3, 4)); } - /// The reads the history, the commit view, the review and the storage inspector ask for + /// The reads the history, the commit view and the review ask for /// go through the session, so they count as running while they run and stop counting /// when dropped. #[gpui::test] @@ -6862,12 +6810,7 @@ mod tests { let path = request.uri().path().to_owned(); let held = seen.clone(); async move { - let feature = path.ends_with("/changes") - || path.ends_with("/healthz") - || path.ends_with("/readyz") - || path.ends_with("/snapshot") - || path == "/graphs"; - if feature { + if path.ends_with("/changes") { // A read that never answers, held until dropped. held.fetch_add(1, Ordering::SeqCst); futures::future::pending::<()>().await; @@ -6897,17 +6840,16 @@ mod tests { ( session.commit_changes("c1", None, 10, cx), session.changes("main", Some("beginning".into()), None, 10, cx), - session.server_state("main", cx), ) }); cx.run_until_parked(); assert_eq!( held.load(Ordering::SeqCst) - held_before, - 6, - "two change reads and four server reads" + 2, + "two change reads" ); session.read_with(cx, |session, _| { - assert_eq!(session.reads_in_flight, before + 6) + assert_eq!(session.reads_in_flight, before + 2) }); drop(reads); cx.run_until_parked(); diff --git a/crates/inspectors/Cargo.toml b/crates/inspectors/Cargo.toml deleted file mode 100644 index 65055711..00000000 --- a/crates/inspectors/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "inspectors" -version.workspace = true -edition.workspace = true -license.workspace = true -publish.workspace = true - -[lib] -path = "src/inspectors.rs" - -[dependencies] -serde_json = { workspace = true } -gpui = { workspace = true } -ui = { workspace = true } -studio_ui = { workspace = true } -studio_actions = { workspace = true } -studio_workspace = { workspace = true } -graph_client = { workspace = true } -graph_store = { workspace = true } - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -graph_store = { workspace = true, features = ["test-support"] } - -[lints] -workspace = true diff --git a/crates/inspectors/src/inspectors.rs b/crates/inspectors/src/inspectors.rs deleted file mode 100644 index e8ece30f..00000000 --- a/crates/inspectors/src/inspectors.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Inspectors for physical state (spec 010): server health and readiness, graph states, the -//! snapshot's manifest version and datasets. -//! -//! Layer 5 (`docs/architecture.md`). Plan: `specs/010-inspectors/plan.md`. - -mod storage; - -use gpui::App; -use studio_actions::graph::OpenInspector; -use studio_workspace::Workspace; - -use storage::{STORAGE_KEY, StorageInspector}; - -/// Registers this surface with every new workspace. -pub fn init(cx: &mut App) { - studio_workspace::register_item_kind(cx, "storage", |_, workspace, window, cx| { - let Some(session) = workspace.session().cloned() else { - return; - }; - workspace.open_item::(STORAGE_KEY, window, cx, |_, cx| { - StorageInspector::new(session, cx) - }); - }); - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|workspace, _: &OpenInspector, window, cx| { - let Some(session) = workspace.session().cloned() else { - return; - }; - // An inspector already open, in any pane, reads again; a new one reads as it is made. - let mut made = false; - let view = workspace.open_item::(STORAGE_KEY, window, cx, |_, cx| { - made = true; - StorageInspector::new(session, cx) - }); - if !made && let Some(view) = view { - view.update(cx, |view, cx| view.reload(cx)); - } - }); - }) - .detach(); -} diff --git a/crates/inspectors/src/storage.rs b/crates/inspectors/src/storage.rs deleted file mode 100644 index d4864de2..00000000 --- a/crates/inspectors/src/storage.rs +++ /dev/null @@ -1,903 +0,0 @@ -//! The "Storage" inspector (spec 010): server health and readiness, every graph's state, and the -//! session branch's snapshot with its datasets. Every value is copyable; a value the server did -//! not send reads "Not reported by this server". The snapshot shown stays when the branch moves: -//! the tab offers the newer one (constitution II, V), and reads again on Refresh or on another -//! branch. - -use std::ops::Range; - -use gpui::{ - AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, - Subscription, Task, Window, uniform_list, -}; -use graph_client::ServerHealth; -use graph_client::api::{ - EntityKind, GraphInfo, GraphListResponse, ReadinessOutput, SnapshotDatasetOutput, - SnapshotOutput, -}; -use graph_store::{GraphSession, Loadable, SessionEvent}; -use studio_actions::workspace::Refresh; -use studio_ui::CopyableValue; -use studio_ui::ds::{Dotted as _, space}; -use studio_workspace::{Item, ItemEvent, SavedItem}; -use ui::prelude::*; -use ui::{TintColor, Tooltip}; - -pub const STORAGE_KEY: &str = "inspector:storage"; - -/// The storage tables' columns, sized to the values they hold; a header and its rows share one. -mod col { - pub const GRAPH: f32 = 140.; - pub const STATE: f32 = 110.; - pub const FLAG: f32 = 60.; - pub const ACTION: f32 = 180.; - pub const FAILURE: f32 = 160.; - pub const KIND: f32 = 56.; - pub const TYPE: f32 = 150.; - pub const ROWS: f32 = 90.; - pub const VERSION: f32 = 80.; - pub const NATIVE: f32 = 130.; -} - -/// The least height of the snapshot's part, in rows (its title, its three facts, the tables' -/// header and some tables), when the server's and the graphs' facts above it would take more: -/// those then scroll in what is left. -const SNAPSHOT_MIN_ROWS: f32 = 12.; - -pub struct StorageInspector { - session: Entity, - branch: String, - focus_handle: FocusHandle, - health: Loadable, - readiness: Loadable, - graphs: Loadable, - /// Its datasets nodes first, each kind by type name, as the table lists them. - snapshot: Loadable, - /// Counts the reads of the snapshot begun: only the latest one's answer is shown, so an - /// older read that finishes later does not put back the storage it saw. - snapshot_reads: u64, - /// After a branch change, the inspector shows the snapshot the session reads for that - /// branch's counts ([`GraphSession::snapshot_of`]) rather than reading the same one itself. - awaiting_counts: bool, - /// The branch head the session knew when the snapshot shown was asked for. The snapshot - /// route names no commit, so a head move is measured against this. - read_at: Option, - /// The branch moved since the snapshot shown was asked for. - newer: bool, - /// Everything is read again when the inspector is next drawn ([`Self::read_owed`]): a tab - /// restored behind another, or one that heard of a new token while hidden, reads when shown. - owes_reload: bool, - _task: Option>, - _snapshot_task: Option>, - _subscription: Subscription, -} - -impl EventEmitter for StorageInspector {} - -impl Focusable for StorageInspector { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for StorageInspector { - fn tab_title(&self, _cx: &App) -> SharedString { - "Storage".into() - } - - fn tab_icon(&self, _cx: &App) -> Option { - Some(IconName::Server) - } - - fn dedupe_key(&self, _cx: &App) -> Option { - Some(STORAGE_KEY.into()) - } - - fn is_stale(&self, _cx: &App) -> bool { - self.newer - } - - fn serialize(&self, _cx: &App) -> Option { - Some(SavedItem { - kind: "storage".into(), - data: serde_json::Value::Null, - }) - } -} - -/// A table's row count as its Rows column shows it: grouped, as Studio writes every count. -fn rows_text(count: i64) -> String { - studio_ui::group_count(count) -} - -/// A snapshot's datasets, nodes first, each kind by type name, as the table lists them. -fn tables_in_order(mut snapshot: SnapshotOutput) -> SnapshotOutput { - snapshot.datasets.sort_by(|a, b| { - (matches!(a.entity_kind, EntityKind::Edge), &a.type_name) - .cmp(&(matches!(b.entity_kind, EntityKind::Edge), &b.type_name)) - }); - snapshot -} - -impl StorageInspector { - pub fn new(session: Entity, cx: &mut Context) -> Self { - let branch = session.read(cx).branch.clone(); - // Its storage is the session branch's: another branch's is read at once. The server's - // health, readiness and graphs are the same on every branch, and stay. - let subscription = cx.subscribe(&session, |this: &mut Self, _, event, cx| match event { - SessionEvent::BranchChanged => this.branch_changed(cx), - SessionEvent::CountsChanged if this.awaiting_counts => this.take_session_snapshot(cx), - // A new token may see less than the one the server's state was read with: none of - // that shows while it is read again under the new one. - SessionEvent::CredentialChanged => this.owe_reload(cx), - // A commit, Studio's or another writer's, moves the tables' versions and rows: the - // storage shown stays, and the tab offers the newer. - SessionEvent::HeadMoved { branch, head } if *branch == this.branch => { - this.head_moved(head, cx) - } - _ => {} - }); - let mut view = Self { - session, - branch, - focus_handle: cx.focus_handle(), - health: Loadable::NotLoaded, - readiness: Loadable::NotLoaded, - graphs: Loadable::NotLoaded, - snapshot: Loadable::NotLoaded, - snapshot_reads: 0, - awaiting_counts: false, - read_at: None, - newer: false, - owes_reload: false, - _task: None, - _snapshot_task: None, - _subscription: subscription, - }; - view.owe_reload(cx); - view - } - - /// Reads everything again when the inspector is next drawn, showing none of what was read - /// meanwhile; reads still out land nowhere. - fn owe_reload(&mut self, cx: &mut Context) { - self.health = Loadable::Loading; - self.readiness = Loadable::Loading; - self.graphs = Loadable::Loading; - self.snapshot = Loadable::Loading; - self.awaiting_counts = false; - self._task = None; - self._snapshot_task = None; - self.owes_reload = true; - cx.notify(); - } - - /// Reads what the inspector owes ([`Self::owe_reload`]): called when it is drawn. - pub fn read_owed(&mut self, cx: &mut Context) { - if self.owes_reload { - self.reload(cx); - } - } - - /// Reads everything again, at the session's current branch. - pub fn reload(&mut self, cx: &mut Context) { - self.owes_reload = false; - self.branch = self.session.read(cx).branch.clone(); - let branch = self.branch.clone(); - self.health = Loadable::Loading; - self.readiness = Loadable::Loading; - self.graphs = Loadable::Loading; - self.snapshot = Loadable::Loading; - let snapshot_read = self.begin_snapshot_read(cx); - let read = self - .session - .update(cx, |session, cx| session.server_state(&branch, cx)); - self._snapshot_task = None; - self._task = Some(cx.spawn(async move |this, cx| { - let state = read.await; - this.update(cx, |this, cx| { - this.health = loadable(state.health); - this.readiness = loadable(state.readiness); - this.graphs = loadable(state.graphs); - // A branch moved to, or a commit, meanwhile is read on its own. - if this.snapshot_reads == snapshot_read { - this.snapshot = loadable(state.snapshot.map(tables_in_order)); - } - cx.notify(); - }) - .ok(); - })); - cx.notify(); - } - - /// The session moved to another branch, and reads that branch's snapshot for its counts: - /// the inspector shows that read once it is in, or why it failed. A session that reads - /// none leaves the inspector to read its own. - fn branch_changed(&mut self, cx: &mut Context) { - self.branch = self.session.read(cx).branch.clone(); - self.snapshot = Loadable::Loading; - self._snapshot_task = None; - self.begin_snapshot_read(cx); - self.awaiting_counts = true; - self.take_session_snapshot(cx); - cx.notify(); - } - - /// The session's snapshot of the inspector's branch, while one is awaited after a branch - /// change: taken when it is in, and its failure shown when it failed (the same request sent - /// again would fail alike; Refresh asks). Read here when the session read none. - fn take_session_snapshot(&mut self, cx: &mut Context) { - let session = self.session.read(cx); - if let Some(snapshot) = session.snapshot_of(&self.branch) { - self.awaiting_counts = false; - self.snapshot = Loadable::Loaded(tables_in_order((*snapshot).clone())); - cx.notify(); - return; - } - match &session.snapshot { - Loadable::Failed(error) => { - let error = error.clone(); - self.awaiting_counts = false; - self.snapshot = Loadable::Failed(error); - cx.notify(); - } - Loadable::NotLoaded => { - self.awaiting_counts = false; - self.reload_snapshot(cx); - } - Loadable::Loading | Loadable::Loaded(_) => {} - } - } - - /// Reads the storage of the session's branch again, the server's facts kept, and the - /// storage read last shown meanwhile. - fn reload_snapshot(&mut self, cx: &mut Context) { - self.branch = self.session.read(cx).branch.clone(); - let branch = self.branch.clone(); - let snapshot_read = self.begin_snapshot_read(cx); - let read = self - .session - .update(cx, |session, cx| session.branch_snapshot(&branch, cx)); - self._snapshot_task = Some(cx.spawn(async move |this, cx| { - let snapshot = read.await; - this.update(cx, |this, cx| { - if this.snapshot_reads == snapshot_read { - this.snapshot = loadable(snapshot.map(tables_in_order)); - cx.notify(); - } - }) - .ok(); - })); - cx.notify(); - } - - /// Counts a read of the branch's snapshot begun, at the head the session knows now, and - /// takes down the offer of a newer one. - fn begin_snapshot_read(&mut self, cx: &mut Context) -> u64 { - self.snapshot_reads += 1; - self.read_at = self.session.read(cx).head(&self.branch).map(str::to_owned); - if self.newer { - self.newer = false; - cx.emit(ItemEvent::UpdateTab); - } - self.snapshot_reads - } - - /// The branch's head is now `head`. A head other than the one the snapshot was asked at - /// offers the newer; the first head the session learns, while the snapshot was read, is - /// taken as the one it was read at. - fn head_moved(&mut self, head: &str, cx: &mut Context) { - match &self.read_at { - None => self.read_at = Some(head.to_owned()), - Some(read_at) if read_at == head || self.newer => {} - Some(_) => { - self.newer = true; - cx.emit(ItemEvent::UpdateTab); - cx.notify(); - } - } - } - - fn render_server(&self, cx: &mut Context) -> AnyElement { - let url = self.session.read(cx).server_url(cx); - let health = self.health.loaded(); - let readiness = self.readiness.loaded(); - let mut rows = vec![ - kv("Address", CopyableValue::new("server-url", Some(url))), - kv( - "Version", - CopyableValue::new("server-version", health.map(|h| h.health.version.clone())), - ), - kv( - "Source", - CopyableValue::new( - "server-source", - health.and_then(|h| h.health.source_version.clone()), - ), - ), - kv( - "HTTP contract", - CopyableValue::new("contract", health.and_then(|h| h.contract.clone())), - ), - kv( - "Storage format", - CopyableValue::new( - "server-schema-version", - health.map(|h| h.health.internal_schema_version.to_string()), - ), - ), - ]; - rows.extend([ - kv( - "Ready", - CopyableValue::new( - "ready", - readiness - .map(|r| format!("{} ({})", if r.ready { "yes" } else { "no" }, r.status)), - ) - .plain(), - ), - kv( - "Graphs", - CopyableValue::new( - "graph-counts", - readiness.map(|r| { - format!( - "{} served · {} ready · {} loading · {} blocked", - r.served_graph_count, - r.ready_graph_count, - r.loading_graph_count, - r.blocked_graph_count - ) - }), - ) - .plain(), - ), - kv( - "State revision", - CopyableValue::new( - "state-revision", - readiness.map(|r| r.state_revision.to_string()), - ), - ), - kv( - "State CAS", - CopyableValue::new("state-cas", readiness.and_then(|r| r.state_cas.clone())), - ), - kv( - "Serving digest", - CopyableValue::new( - "serving-digest", - readiness.and_then(|r| r.booted_serving_digest.clone()), - ), - ), - kv( - "Shutdown grace", - CopyableValue::new( - "grace", - readiness.map(|r| format!("{} s", r.shutdown_grace_seconds)), - ) - .plain(), - ), - ]); - section( - "Server", - error_of(&[&self.health.failed(), &self.readiness.failed()]), - rows, - cx, - ) - } - - fn render_graphs(&self, cx: &mut Context) -> AnyElement { - let current = self.session.read(cx).graph_id.clone(); - let rows = match self.graphs.loaded() { - Some(list) => list - .graphs - .iter() - .enumerate() - .map(|(ix, graph)| graph_row(ix, graph, graph.graph_id == current)) - .collect(), - None => Vec::new(), - }; - let header = h_flex() - .gap(px(space::MD)) - .child(column("Graph", col::GRAPH)) - .child(column("State", col::STATE)) - .child(column("Reads", col::FLAG)) - .child(column("Writes", col::FLAG)) - .child(column("Action", col::ACTION)) - .child(column("Failure", col::FAILURE)) - .child(div().flex_1().child(heading("Storage root"))) - .into_any_element(); - let mut all = vec![header]; - all.extend(rows); - section("Graphs", error_of(&[&self.graphs.failed()]), all, cx) - } - - /// The snapshot's facts over its tables. A graph can hold hundreds of tables (653 on one - /// seen), so only the rows in view are built, each with its copy buttons, as every long - /// list is drawn (constitution IX); the list scrolls in the room the facts above leave. - fn render_snapshot(&self, cx: &mut Context) -> AnyElement { - let snapshot = self.snapshot.loaded(); - let facts = [ - kv( - "Branch", - CopyableValue::new("snapshot-branch", snapshot.map(|s| s.graph_branch.clone())), - ), - kv( - "Manifest version", - CopyableValue::new( - "manifest-version", - snapshot.map(|s| s.graph_manifest_version.to_string()), - ), - ), - kv( - "Storage format", - CopyableValue::new( - "snapshot-schema-version", - snapshot.map(|s| s.internal_schema_version.to_string()), - ), - ), - ]; - let tables = snapshot.map(|snapshot| { - v_flex() - .flex_1() - .min_h_0() - .child( - h_flex() - .gap(px(space::MD)) - .pt(px(space::MD)) - .child(column("Kind", col::KIND)) - .child(column("Type", col::TYPE)) - .child(column("Rows", col::ROWS)) - .child(column("Version", col::VERSION)) - .child(column("Native branch", col::NATIVE)) - .child(div().flex_1().child(heading("Dataset path"))), - ) - .child( - uniform_list( - "storage-tables", - snapshot.datasets.len(), - cx.processor(|this, range: Range, _window, cx| { - this.dataset_rows(range, cx) - }), - ) - .flex_1() - .min_h_0(), - ) - }); - let error = error_of(&[&self.snapshot.failed()]); - v_flex() - .w_full() - .flex_1() - .min_h(px(studio_ui::ds::size::SIDEBAR_ROW * SNAPSHOT_MIN_ROWS)) - .gap(px(space::XS)) - .py(px(space::LG)) - .border_b_1() - .dotted(cx) - .child(studio_ui::ds::text::muted(format!( - "Snapshot of {}", - self.branch - ))) - .children(error.map(|e| studio_ui::ds::text::error(format!("Could not read: {e}")))) - .children(facts) - .children(tables) - .into_any_element() - } - - /// The rows of the snapshot's tables in `range`, as the list shows them. - fn dataset_rows(&self, range: Range, cx: &App) -> Vec { - let Some(snapshot) = self.snapshot.loaded() else { - return Vec::new(); - }; - range - .filter_map(|ix| Some(dataset_row(ix, snapshot.datasets.get(ix)?, cx))) - .collect() - } -} - -/// One table of the snapshot: its kind, type, rows, version, native branch and path, each -/// copyable. One line tall whatever it holds, as the list it is drawn in requires. -fn dataset_row(ix: usize, dataset: &SnapshotDatasetOutput, cx: &App) -> AnyElement { - h_flex() - .gap(px(space::MD)) - .h(px(studio_ui::ds::size::SIDEBAR_ROW)) - .child( - div() - .w(px(col::KIND)) - .flex_none() - .child(studio_ui::ds::text::muted(match dataset.entity_kind { - EntityKind::Node => "node", - EntityKind::Edge => "edge", - })), - ) - .child( - h_flex() - .w(px(col::TYPE)) - .flex_none() - .gap(px(space::SM)) - .child(studio_ui::kind_swatch( - &dataset.type_name, - dataset.entity_kind == EntityKind::Edge, - cx, - )) - .child(CopyableValue::new(("type", ix), Some(dataset.type_name.clone())).plain()), - ) - .child(div().w(px(col::ROWS)).flex_none().child(CopyableValue::new( - ("rows", ix), - Some(rows_text(dataset.entity_count)), - ))) - .child( - div() - .w(px(col::VERSION)) - .flex_none() - .child(CopyableValue::new( - ("version", ix), - Some(dataset.published_dataset_version.to_string()), - )), - ) - .child( - div() - .w(px(col::NATIVE)) - .flex_none() - .child(CopyableValue::new( - ("native", ix), - Some( - dataset - .native_dataset_branch - .clone() - .unwrap_or_else(|| "none".into()), - ), - )), - ) - .child(div().flex_1().min_w_0().child(CopyableValue::new( - ("path", ix), - Some(dataset.dataset_path.clone()), - ))) - .into_any_element() -} - -trait FailedMessage { - fn failed(&self) -> Option; -} - -impl FailedMessage for Loadable { - fn failed(&self) -> Option { - match self { - Loadable::Failed(message) => Some(message.clone()), - _ => None, - } - } -} - -fn loadable(result: Result) -> Loadable { - match result { - Ok(value) => Loadable::Loaded(value), - Err(error) => Loadable::Failed(error.to_string()), - } -} - -fn error_of(errors: &[&Option]) -> Option { - let messages: Vec<&str> = errors.iter().filter_map(|e| e.as_deref()).collect(); - (!messages.is_empty()).then(|| messages.join(" · ")) -} - -fn heading(text: &'static str) -> Label { - studio_ui::ds::text::meta(text) -} - -fn column(text: &'static str, width: f32) -> Div { - div().w(px(width)).flex_none().child(heading(text)) -} - -fn kv(label: &'static str, value: CopyableValue) -> AnyElement { - h_flex() - .gap(px(space::LG)) - .min_h(px(studio_ui::ds::size::SIDEBAR_ROW)) - .child( - div() - .w(px(studio_ui::ds::size::FIELD_LABEL)) - .flex_none() - .child(studio_ui::ds::text::muted(label)), - ) - .child(div().flex_1().min_w_0().child(value)) - .into_any_element() -} - -fn graph_row(ix: usize, graph: &GraphInfo, current: bool) -> AnyElement { - let state = format!("{:?}", graph.state).to_lowercase(); - let action = words(&format!("{:?}", graph.action)); - let failure = graph.failure.map(|f| words(&format!("{f:?}"))); - let yes_no = |b: bool| if b { "yes" } else { "no" }; - h_flex() - .gap(px(space::MD)) - .min_h(px(studio_ui::ds::size::SIDEBAR_ROW)) - .child( - h_flex() - .w(px(col::GRAPH)) - .flex_none() - .gap(px(space::XS)) - .child(CopyableValue::new( - ("graph-id", ix), - Some(graph.graph_id.clone()), - )) - .when(current, |d| { - d.child(studio_ui::ds::text::meta("open").color(Color::Accent)) - }), - ) - .child( - div() - .w(px(col::STATE)) - .flex_none() - .child(CopyableValue::new(("graph-state", ix), Some(state)).plain()), - ) - .child( - div() - .w(px(col::FLAG)) - .flex_none() - .child(studio_ui::ds::text::body(yes_no(graph.read_available))), - ) - .child( - div() - .w(px(col::FLAG)) - .flex_none() - .child(studio_ui::ds::text::body(yes_no(graph.write_available))), - ) - .child( - div() - .w(px(col::ACTION)) - .flex_none() - .child(CopyableValue::new(("graph-action", ix), Some(action)).plain()), - ) - .child( - div() - .w(px(col::FAILURE)) - .flex_none() - .child(studio_ui::ds::text::muted( - failure.unwrap_or_else(|| "none".into()), - )), - ) - .child(div().flex_1().min_w_0().child(CopyableValue::new( - ("graph-uri", ix), - Some(graph.uri.clone()), - ))) - .into_any_element() -} - -/// `WaitForStartup` → `wait for startup`. -fn words(variant: &str) -> String { - let mut out = String::new(); - for (i, c) in variant.chars().enumerate() { - if c.is_uppercase() && i > 0 { - out.push(' '); - } - out.extend(c.to_lowercase()); - } - out -} - -fn section(title: &str, error: Option, rows: Vec, cx: &App) -> AnyElement { - v_flex() - .w_full() - .gap(px(space::XS)) - .py(px(space::LG)) - .border_b_1() - .dotted(cx) - .child(studio_ui::ds::text::muted(title.to_owned())) - .children(error.map(|e| studio_ui::ds::text::error(format!("Could not read: {e}")))) - .children(rows) - .into_any_element() -} - -impl Render for StorageInspector { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.owes_reload { - cx.defer_in(window, |this, _, cx| this.read_owed(cx)); - } - let graph_id = self.session.read(cx).graph_id.clone(); - let loading = self.snapshot.is_loading(); - let server = self.render_server(cx); - let graphs = self.render_graphs(cx); - let snapshot = self.render_snapshot(cx); - v_flex() - .key_context("StorageInspector") - .track_focus(&self.focus_handle) - // ⌘R reads the inspector again, and goes on to the workspace, which reads the - // graph's schema, branches and counts again. - .on_action(cx.listener(|this, _: &Refresh, _, cx| { - this.reload(cx); - cx.propagate(); - })) - .size_full() - .child( - studio_ui::ds::toolbar(cx) - .child( - studio_ui::ds::icon::lead(IconName::Server), - ) - .child(studio_ui::ds::text::title(format!( - "Storage · {graph_id} @ {}", - self.branch - ))) - .when(loading, |d| d.child(studio_ui::ds::text::meta("Reading…"))) - .when(self.newer, |d| { - d.child( - studio_ui::ds::button( - "newer-storage", - "Newer snapshot · Refresh", - ) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text( - "The branch moved since its storage was read; read it again", - )) - .on_click( - cx.listener(|this, _, _, cx| this.reload_snapshot(cx)), - ), - ) - }) - .child(div().flex_1()) - .child( - studio_ui::ds::icon_button("refresh-storage", IconName::RotateCw) - .tooltip(|_, cx| { - Tooltip::for_action("Read the server's state again", &Refresh, cx) - }) - .on_click(cx.listener(|this, _, _, cx| this.reload(cx))), - ), - ) - .child( - v_flex() - .flex_1() - .min_h_0() - .px(px(space::XXL)) - .py(px(space::XL)) - .gap(px(space::XL)) - // The facts give way to the snapshot's tables, and scroll when they do. - .child( - v_flex() - .id("storage-facts") - .flex_shrink_1() - .min_h_0() - .overflow_y_scroll() - .gap(px(space::XL)) - .child(server) - .child(graphs), - ) - .child(snapshot) - .child( - div() - .id("index-status") - .tooltip(ui::Tooltip::text( - "Omnigraph does not serve index status yet; `explain` in the query editor shows a type's index facts", - )) - .child(studio_ui::ds::meta(IconName::Info, "Index status not served")), - ), - ) - } -} - -#[cfg(test)] -mod tests { - use gpui::TestAppContext; - use serde_json::json; - - use super::*; - - #[test] - fn variants_read_as_words() { - assert_eq!(words("WaitForStartup"), "wait for startup"); - assert_eq!(words("None"), "none"); - } - - /// Another writer's commit leaves the storage shown and offers the newer; Refresh reads it - /// again, and another branch is shown at once, from the snapshot the session reads for its - /// counts rather than a second read of it. - #[gpui::test] - async fn a_head_move_offers_the_newer_snapshot_and_reads_it_only_on_refresh( - cx: &mut TestAppContext, - ) { - let (session, served) = graph_store::test_server::followed_session( - "node Person {\n slug: String @key\n}\n", - |_| json!([]), - cx, - ); - served.lock().unwrap().branches.push("feature".into()); - cx.run_until_parked(); - let inspector = cx.new(|cx| StorageInspector::new(session.clone(), cx)); - cx.run_until_parked(); - // Nothing is read until the inspector is drawn. - assert_eq!( - inspector.read_with(cx, |inspector, _| inspector.snapshot_reads), - 0 - ); - inspector.update(cx, |inspector, cx| inspector.read_owed(cx)); - cx.run_until_parked(); - let state = |cx: &mut TestAppContext| { - inspector.read_with(cx, |inspector, cx| { - ( - inspector.snapshot_reads, - inspector.newer, - inspector.is_stale(cx), - ) - }) - }; - assert_eq!(state(cx), (1, false, false)); - - served.lock().unwrap().commit("c2", Vec::new()); - session.update(cx, |session, cx| session.check_heads(cx)); - cx.run_until_parked(); - assert_eq!(state(cx), (1, true, true), "the storage shown stays"); - - inspector.update(cx, |inspector, cx| inspector.reload_snapshot(cx)); - cx.run_until_parked(); - assert_eq!(state(cx), (2, false, false)); - // The head the snapshot was read at, seen again, offers nothing. - session.update(cx, |session, cx| session.check_heads(cx)); - cx.run_until_parked(); - assert_eq!(state(cx), (2, false, false)); - - let snapshots = served.lock().unwrap().snapshot_reads; - session.update(cx, |session, cx| session.set_branch("feature".into(), cx)); - cx.run_until_parked(); - assert_eq!( - state(cx), - (3, false, false), - "another branch is shown at once" - ); - inspector.read_with(cx, |inspector, _| { - assert_eq!(inspector.branch, "feature"); - assert!( - !matches!(inspector.snapshot, Loadable::Loading), - "the session's read, shown" - ); - }); - assert_eq!( - served.lock().unwrap().snapshot_reads, - snapshots + 1, - "the session's read of the branch's counts, shown here too" - ); - } - - /// A new token on the connection reads the server's state again under it, showing none of - /// the old token's meanwhile: it may see less. - #[gpui::test] - async fn a_new_token_reads_the_storage_again(cx: &mut TestAppContext) { - let (session, _) = graph_store::test_server::followed_session( - "node Person {\n slug: String @key\n}\n", - |_| json!([]), - cx, - ); - cx.run_until_parked(); - let inspector = cx.new(|cx| StorageInspector::new(session.clone(), cx)); - inspector.update(cx, |inspector, cx| inspector.read_owed(cx)); - cx.run_until_parked(); - let reads = |cx: &mut TestAppContext| { - inspector.read_with(cx, |inspector, _| inspector.snapshot_reads) - }; - assert_eq!(reads(cx), 1); - let connection = session.read_with(cx, |session, _| session.connection.clone()); - connection.update(cx, |connection, cx| { - connection.set_token(Some("another".into()), cx) - }); - inspector.read_with(cx, |inspector, _| { - assert!( - inspector.graphs.is_loading(), - "the old token's graphs do not show" - ); - assert!(inspector.snapshot.is_loading()); - }); - cx.run_until_parked(); - assert_eq!(reads(cx), 1, "read again only once drawn"); - inspector.update(cx, |inspector, cx| inspector.read_owed(cx)); - cx.run_until_parked(); - assert_eq!(reads(cx), 2, "read again under the new token"); - inspector.read_with(cx, |inspector, _| { - assert!(!inspector.graphs.is_loading()); - }); - } - - /// A table's row count is grouped as every count Studio writes. - #[test] - fn row_counts_are_grouped() { - assert_eq!(rows_text(12_480), "12,480"); - assert_eq!(rows_text(7), "7"); - } -} diff --git a/crates/studio/Cargo.toml b/crates/studio/Cargo.toml index c058cb41..a9d0d3b0 100644 --- a/crates/studio/Cargo.toml +++ b/crates/studio/Cargo.toml @@ -54,7 +54,6 @@ query_editor = { workspace = true } agent_panel = { workspace = true } review = { workspace = true } history = { workspace = true } -inspectors = { workspace = true } # Text needs `font-kit` on macOS; Linux needs a windowing backend. [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/studio/src/lib.rs b/crates/studio/src/lib.rs index 5d0b8537..ccb6be87 100644 --- a/crates/studio/src/lib.rs +++ b/crates/studio/src/lib.rs @@ -50,7 +50,6 @@ pub fn init(cx: &mut App) { query_editor::init(cx); review::init(cx); history::init(cx); - inspectors::init(cx); agent_panel::init(cx); studio_workspace::register_item_kind(cx, "design-system", |_, workspace, window, cx| { @@ -1126,7 +1125,6 @@ mod start_tests { query_editor::init(cx); review::init(cx); history::init(cx); - inspectors::init(cx); agent_panel::init(cx); }); let (_workspace, cx) = cx.add_window_view(Workspace::new); diff --git a/crates/studio/src/menus.rs b/crates/studio/src/menus.rs index bbfab9ed..142b4bf6 100644 --- a/crates/studio/src/menus.rs +++ b/crates/studio/src/menus.rs @@ -4,8 +4,8 @@ use gpui::{App, Menu, MenuItem, OsAction, SystemMenuType}; use studio_actions::agent::OpenAgent; use studio_actions::graph::{ - CreateBranch, NewQuery, OpenCanvas, OpenHistory, OpenInspector, OpenOntology, OpenQueryFile, - OpenSchema, ReviewBranch, + CreateBranch, NewQuery, OpenCanvas, OpenHistory, OpenOntology, OpenQueryFile, OpenSchema, + ReviewBranch, }; use studio_actions::query::{Save, SaveAs}; use studio_actions::update::{CheckForUpdates, RestartToUpdate}; @@ -91,7 +91,6 @@ pub fn set(cx: &mut App) { MenuItem::action("Ontology", OpenOntology), MenuItem::action("Schema", OpenSchema::default()), MenuItem::action("Canvas", OpenCanvas), - MenuItem::action("Storage Inspector", OpenInspector), MenuItem::action("Agent", OpenAgent), MenuItem::separator(), MenuItem::action("Create Branch…", CreateBranch), diff --git a/crates/studio/tests/keymaps.rs b/crates/studio/tests/keymaps.rs index f7bc07fa..46fe8014 100644 --- a/crates/studio/tests/keymaps.rs +++ b/crates/studio/tests/keymaps.rs @@ -30,7 +30,7 @@ const KEYMAPS: [(&str, &str); 3] = [ /// The actions without a default key, as in Zed: rare, or destructive enough that a key would /// be reached by accident. Each is in the command palette, and most in a menu as well. -const PALETTE_ONLY: [&str; 21] = [ +const PALETTE_ONLY: [&str; 20] = [ "studio::About", "studio::RevealLog", "studio::OpenDesignSystem", @@ -41,7 +41,6 @@ const PALETTE_ONLY: [&str; 21] = [ "studio::CloseGraph", "studio_graph::ReviewBranch", "studio_graph::OpenHistory", - "studio_graph::OpenInspector", "studio_graph::CreateBranch", "studio_graph::DeleteBranch", "studio_agent::ClearQueue", diff --git a/crates/studio_actions/src/studio_actions.rs b/crates/studio_actions/src/studio_actions.rs index 9dcf58a3..55b2d5fd 100644 --- a/crates/studio_actions/src/studio_actions.rs +++ b/crates/studio_actions/src/studio_actions.rs @@ -163,8 +163,6 @@ pub mod graph { OpenCanvas, /// Open the ontology: the graph's types in domains, one type at a time. OpenOntology, - /// Open the storage inspector: the server's health and the graph's storage. - OpenInspector, /// Create a branch from the current branch. CreateBranch, /// Delete the current branch. diff --git a/docs/architecture.md b/docs/architecture.md index ac3232db..94ba8f40 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ other: what two features share moves down a layer. | 3 | `studio_update` | The updater (spec 017): checks Studio's own releases, downloads, verifies and installs an update. The only requests not to an Omnigraph server or the sign-in service it names | `gpui`, `studio_settings`, `http_client`, `minisign-verify` | | 4 | `studio_workspace` | The window frame: panes, tabs, docks, panels, status bar, modal and toast layers | `gpui`, `ui`, `studio_ui`, `studio_actions`, `studio_settings` | | 4 | `change_cards` | The entity cards a review and a commit's view share: an entity's card, its relationship lines, a run's header, how many lines a card shows | `gpui`, `ui`, `studio_ui`, `graph_store`, `gq_language` | -| 5 | `command_palette`, `type_table`, `relationship_tree`, `graph_canvas`, `query_editor`, `review`, `history`, `inspectors`, `agent_panel` | One surface each | layers 0–4 | +| 5 | `command_palette`, `type_table`, `relationship_tree`, `graph_canvas`, `query_editor`, `review`, `history`, `agent_panel` | One surface each | layers 0–4 | | 6 | `studio` | The binary: initialises every crate in order, menus, windows, single instance, URL scheme | everything | `studio_workspace` is forked and trimmed from Zed's `workspace`, without projects, @@ -40,7 +40,7 @@ view (layer 5) ──reads──▶ entity in graph_store ──calls──▶ g - Views never call `graph_client` directly. They read entities and dispatch actions. The session's client is private to `graph_store`, so every read goes through a session method that counts it while it runs and notes what it says about the server (the history's and the - review's change pages, the storage inspector's server state among them). The one client + review's change pages among them). The one client another crate holds is the agent gateway's (`GraphSession::gateway_client`), which forwards the agent CLI's own requests inside its sandbox. - `graph_client` sends through Zed's `HttpClient` (the `reqwest_client` that `graph_store::init` diff --git a/script/check-layers.py b/script/check-layers.py index 8224221e..615a2226 100755 --- a/script/check-layers.py +++ b/script/check-layers.py @@ -27,7 +27,6 @@ "query_editor": 5, "review": 5, "history": 5, - "inspectors": 5, "agent_panel": 5, "studio": 6, } diff --git a/specs/002-app-shell/plan.md b/specs/002-app-shell/plan.md index 1b8856ef..b3a67bad 100644 --- a/specs/002-app-shell/plan.md +++ b/specs/002-app-shell/plan.md @@ -220,8 +220,8 @@ what was last saved at most every 250 ms rather than serializing every tab on ev `Workspace`, `TypeTable`, `Tree`, `Canvas`, `QueryEditor`, `Review`, `History`, `Inspector`. Every action without arguments has a key on every platform except, as in Zed, the rare and the destructive (reveal the log, the design system gallery, directional splits left and up, - pin and keep tab, close graph, branch create, delete, merge and review, history, the storage - inspector, the agent's queue, restart and conversation list), which the palette and menus + pin and keep tab, close graph, branch create, delete, merge and review, history, the agent's + queue, restart and conversation list), which the palette and menus reach. `crates/studio/tests/keymaps.rs` holds that list and fails on an action outside it with no key. Workspace keys from Zed: ⌥⌘O open a graph (Zed's open recent), ⌥⌘T close others, ⌘R refresh the graph (a table's own ⌘R refreshes the table); ⌥⌘W closes every tab, since ⌘K is diff --git a/specs/010-inspectors/plan.md b/specs/010-inspectors/plan.md index 2155de8f..50822113 100644 --- a/specs/010-inspectors/plan.md +++ b/specs/010-inspectors/plan.md @@ -1,7 +1,8 @@ # 010: Inspectors -Status: built on 0.13 · 2026-10-12 (`crates/inspectors`): a "Storage" tab. Index facts wait on -E13: 0.13's `explain` names none for a scan (see the validation log). +Status: superseded · removed 2026-10-31, to cut what Studio shows to what its work needs (see +`ui-ux-audit-report.md`). Built on 0.13 · 2026-10-12 (`crates/inspectors`): a "Storage" tab. +The last commit with it is `8b26a45`. ## Problem diff --git a/specs/README.md b/specs/README.md index 43563836..4c2edd6a 100644 --- a/specs/README.md +++ b/specs/README.md @@ -40,7 +40,7 @@ Add tasks, contracts or data-model files only for large features. | 007 | [Query editor](007-query-editor/plan.md) | first slice landed | | 008 | [Branches, review and merge](008-branches-review-merge/plan.md) | built on 0.13 | | 009 | [History](009-history/plan.md) | built on 0.13 | -| 010 | [Inspectors](010-inspectors/plan.md) | built on 0.13 | +| 010 | [Inspectors](010-inspectors/plan.md) | superseded: removed 2026-10-31 | | 011 | [Agent panel](011-agent-panel/plan.md) | implemented | | 012 | [Omnigraph engine changes](012-omnigraph-engine-changes/plan.md) | deferred | | 013 | [The agent uses the Omnigraph CLI](013-agent-cli/plan.md) | implemented |