agentmux_srv/backend/wcore/block.rs
1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! Block CRUD operations.
6
7use uuid::Uuid;
8
9use crate::backend::storage::store::Store;
10use crate::backend::storage::StoreError;
11use crate::backend::obj::*;
12
13/// Create a new block in a tab.
14pub fn create_block(
15 store: &Store,
16 tab_id: &str,
17 meta: MetaMapType,
18) -> Result<Block, StoreError> {
19 let mut tab = store.must_get::<Tab>(tab_id)?;
20
21 let mut block = Block {
22 oid: Uuid::new_v4().to_string(),
23 parentoref: format!("tab:{}", tab_id),
24 meta,
25 ..Default::default()
26 };
27 store.insert(&mut block)?;
28
29 tab.blockids.push(block.oid.clone());
30 store.update(&mut tab)?;
31
32 Ok(block)
33}
34
35/// Delete a block's rows: remove it from its parent tab's `blockids` and
36/// delete the Block row.
37///
38/// SPEC_864 site #6 — the layout-tree prune that used to live here
39/// (wcore-direct `LayoutState` write, the last Path-B writer on the
40/// block-delete path) moved to the reducer: the `delete_block` saga
41/// dispatches `LayoutDeleteNodeByBlock` as its Step 2, and the persist
42/// subscriber writes the reducer's resulting tree from the
43/// `LayoutNodeDeleted{new_tree, tree_cleared}` event. This fn is row-ops
44/// only, keeping `db_layout` single-writer. SPEC_864 Phase 5 deleted the
45/// `prune_block_from_layout`/`heal_layout` backstop that used to live
46/// below — no Path-B writer remains to produce the orphans it swept.
47pub fn delete_block(
48 store: &Store,
49 tab_id: &str,
50 block_id: &str,
51) -> Result<(), StoreError> {
52 let mut tab = store.must_get::<Tab>(tab_id)?;
53 tab.blockids.retain(|id| id != block_id);
54 store.update(&mut tab)?;
55 store.delete::<Block>(block_id)?;
56 // An agent's ownership of a browser pane ends with the pane
57 // (SPEC_AGENT_DRIVEN_BROWSER_PANES_2026_10_07.md §3).
58 crate::server::browser_owner::forget(block_id);
59 // So does anything it was waiting on the user for: an agent mid
60 // hand-off or approval gets Cancelled now, not after the timeout.
61 crate::server::browser_attention::cancel_for(block_id);
62 // And its share of a site limit (its chain's other panes keep theirs).
63 crate::server::browser_allowlist::drop_pane(block_id);
64 // The host drops an Incognito jar once no tab uses it: tell it now.
65 crate::server::browser_owner::changed().notify_one();
66 Ok(())
67}
68
69/// Resolve a block ID from an 8-character prefix within a tab.
70pub fn resolve_block_id_from_prefix(
71 store: &Store,
72 tab_id: &str,
73 prefix: &str,
74) -> Result<String, StoreError> {
75 if prefix.len() != 8 {
76 return Err(StoreError::Other(
77 "block_id prefix must be 8 characters".to_string(),
78 ));
79 }
80 let tab = store.must_get::<Tab>(tab_id)?;
81 for block_id in &tab.blockids {
82 if block_id.starts_with(prefix) {
83 return Ok(block_id.clone());
84 }
85 }
86 Err(StoreError::NotFound)
87}