app
Adom Video Post-Production
Public Made by Adomby adom
AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
//! Hydrogen webview tab lifecycle helpers.
//!
//! Every webapp-backed subcommand wants the same three things on
//! startup/shutdown: pick a good target pane (NEVER the VS Code
//! pane — see the rule in the adom-workspace-control skill), spawn
//! a webview tab pointed at our local HTTP server, and (on shutdown)
//! remove the tab by name.
//!
//! The pane-picking rule: walk the workspace layout tree, find every
//! leaf, filter to leaves whose tabs do NOT include a VS Code tab
//! (panelType `adom/a1b2c3d4-eeee-4000-a000-00000000000e`), and pick
//! one of those. Only fall back to the currently-focused pane when
//! every pane has VS Code in it (single-pane layout). Never ever
//! target the VS Code pane — it hosts the Claude Code chat, and
//! stacking a webview over it hides the user's primary interaction
//! surface. This bug was shipped for everyone before the rule was
//! documented; don't regress it.
use std::process::Command;
/// Canonical VS Code panelType. Any tab carrying this type is a
/// VS Code / code-server instance hosting the Claude Code chat.
const VSCODE_PANEL_TYPE: &str = "adom/a1b2c3d4-eeee-4000-a000-00000000000e";
/// Open a Hydrogen webview tab pointed at `url` with the given
/// display name and MDI icon. Uses `panel_id_override` if set,
/// otherwise picks a target pane that does NOT contain VS Code.
///
/// If a tab with `name` already exists (from a prior session, a
/// crashed instance, or a leftover from the legacy Python pipeline),
/// it is removed first so the new tab can land cleanly. This makes
/// every restart self-healing — the user doesn't have to manually
/// close stale tabs to recover.
pub fn open(
name: &str,
url: &str,
display_icon: &str,
panel_id_override: Option<&str>,
) -> Result<(), String> {
// Remove any pre-existing tab with the same name. The remove call
// returns 404 if no such tab exists, which we ignore — both "no
// stale tab" and "successfully removed stale tab" are good
// outcomes here.
let _ = Command::new("adom-cli")
.args(["hydrogen", "workspace", "remove-tab", "--name", name])
.output();
let panel_id = match panel_id_override {
Some(p) => p.to_string(),
None => pick_non_vscode_panel_id()?,
};
let initial_state = format!(r#"{{"url":"{}"}}"#, url);
let result = Command::new("adom-cli")
.args([
"hydrogen",
"workspace",
"add-tab",
"--panel-id",
&panel_id,
"--panel-type",
"adom/a1b2c3d4-0031-4000-a000-000000000031",
"--display-name",
name,
"--display-icon",
display_icon,
"--initial-state",
&initial_state,
])
.output()
.map_err(|e| format!("adom-cli spawn failed: {}", e))?;
if !result.status.success() {
return Err(format!(
"failed to add webview tab: {}",
String::from_utf8_lossy(&result.stderr)
));
}
// NB: intentionally NOT calling `adom-cli hydrogen workspace
// active-tab --name <same name>` here. Doing that right after
// add-tab silently wipes the displayIcon out of the just-added
// tab's panelState (verified live on 2026-04-15). add-tab
// already activates the new tab by default (its own _hint
// output says "Tab ... created and active"), so the follow-up
// call is redundant anyway. This is a latent bug in adom-cli
// or Hydrogen that should be filed separately.
Ok(())
}
/// Remove the webview tab with the given display name.
pub fn remove(name: &str) -> Result<(), String> {
let result = Command::new("adom-cli")
.args(["hydrogen", "workspace", "remove-tab", "--name", name])
.output()
.map_err(|e| format!("adom-cli spawn failed: {}", e))?;
if !result.status.success() {
return Err(format!(
"failed to remove webview tab: {}",
String::from_utf8_lossy(&result.stderr)
));
}
Ok(())
}
/// Resolve a server port to the correct URL for the Hydrogen webview
/// to fetch. Inside Coder/Gallia containers the webview iframe is
/// same-origin with hydrogen.adom.inc and can't load `http://127.0.0.1:...`
/// directly — `VSCODE_PROXY_URI` is set to a template like
/// `https://<slug>.adom.cloud/proxy/{{port}}/` which the webview CAN
/// reach. Falls back to the loopback URL when the env var is unset.
pub fn resolve_url(port: u16) -> String {
match std::env::var("VSCODE_PROXY_URI") {
Ok(template) => template.replace("{{port}}", &port.to_string()),
Err(_) => format!("http://127.0.0.1:{}/", port),
}
}
/// Query the workspace layout and pick a target pane that does
/// NOT contain a VS Code tab. Preference order:
/// 1. The currently-focused pane IF it is not VS Code.
/// 2. The first non-VS-Code pane in tree order.
/// 3. The focused pane (fallback, single-pane layout — caller
/// will land a tab on top of VS Code and should hopefully
/// have already warned the user).
///
/// See the adom-workspace-control skill, "NEVER place a new tab on
/// the same pane that hosts VS Code" for the reasoning.
pub fn pick_non_vscode_panel_id() -> Result<String, String> {
let result = Command::new("adom-cli")
.args(["hydrogen", "workspace", "get"])
.output()
.map_err(|e| format!("adom-cli spawn failed: {}", e))?;
if !result.status.success() {
return Err(format!(
"failed to query workspace: {}",
String::from_utf8_lossy(&result.stderr)
));
}
let body = String::from_utf8_lossy(&result.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&body).map_err(|e| format!("failed to parse workspace JSON: {}", e))?;
let focused = parsed
.get("focusedPanelId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let root = parsed
.get("root")
.ok_or_else(|| "workspace response has no root".to_string())?;
let mut leaves: Vec<&serde_json::Value> = Vec::new();
collect_leaves(root, &mut leaves);
// Filter out any leaf that has a VS Code tab.
let non_vscode: Vec<&serde_json::Value> = leaves
.iter()
.copied()
.filter(|leaf| !leaf_contains_vscode(leaf))
.collect();
if non_vscode.is_empty() {
// Single-pane layout — we have no safe choice. Log a warning
// and return the focused pane so the caller at least doesn't
// fail outright.
eprintln!(
"warning: every workspace pane contains a VS Code tab; falling back to focused pane. \
The new webview will land on top of VS Code. Split a pane first to avoid this."
);
return focused.ok_or_else(|| "no focusedPanelId in workspace response".to_string());
}
// Prefer the focused pane IF it is itself non-VS-Code.
if let Some(f) = &focused {
for leaf in &non_vscode {
if leaf.get("id").and_then(|v| v.as_str()) == Some(f.as_str()) {
return Ok(f.clone());
}
}
}
// Otherwise take the first non-VS-Code pane in tree order.
non_vscode[0]
.get("id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| "non-VS-Code leaf had no id".to_string())
}
/// Recursive helper: walk the workspace tree and append every leaf
/// (node with a `tabs` array) to `out`.
fn collect_leaves<'a>(node: &'a serde_json::Value, out: &mut Vec<&'a serde_json::Value>) {
if node.get("tabs").is_some() {
out.push(node);
return;
}
if let Some(first) = node.get("first") {
collect_leaves(first, out);
}
if let Some(second) = node.get("second") {
collect_leaves(second, out);
}
}
/// Return true if any tab in the leaf carries the VS Code panelType.
fn leaf_contains_vscode(leaf: &serde_json::Value) -> bool {
leaf.get("tabs")
.and_then(|t| t.as_array())
.map(|tabs| {
tabs.iter().any(|tab| {
tab.get("panelType").and_then(|v| v.as_str()) == Some(VSCODE_PANEL_TYPE)
})
})
.unwrap_or(false)
}