aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils/src/kdl
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 17:05:34 +0200
committeruser <user@clank>2026-08-12 17:05:34 +0200
commitdf052db4608f4d835fe7b4ca76dbb4059e78943b (patch)
tree639c58957611a01b8674ebd0ce9c123482d6b846 /zellij-utils/src/kdl
parentsl0p: propose per-pane workspace hints (diff)
downloadsl0ppij-df052db4608f4d835fe7b4ca76dbb4059e78943b.tar.gz
sl0ppij-df052db4608f4d835fe7b4ca76dbb4059e78943b.tar.xz
sl0ppij-df052db4608f4d835fe7b4ca76dbb4059e78943b.zip
sl0p: per-pane workspace hints (purpose=)
Implements the zellij half of sl0p/workspace-hints-proposal.md: a pane can carry an opaque agent-facing hint for what it is FOR, so a project can ship a layout that an agent is able to navigate instead of guessing. zellij never interprets a purpose. The consuming agent owns the entire vocabulary, which is what keeps this to one string threaded along the existing status_bar path rather than a policy engine, and lets the vocabulary change without a rebuild. Two ways in, because declared panes and agent-created panes are different: layout: pane name="server" purpose="dev-server" in-band: printf '\033]5577;1;purpose;agent:build\033\\' The second exists because tagging at spawn time would mean threading a field through 28 SpawnTerminal call sites, and a new action variant is impossible while protobuf enums cannot be regenerated. Wrapping the command costs one printf. A declared purpose outranks an in-band one and cannot be overridden or cleared by it. OSC arrives from whatever the pane happens to be showing, so otherwise a `cat hostile.txt` in the human's editor pane could relabel it as the agent's scratch pane and invite an agent to run commands where the human is working. A layout is written by a human in a file they can read; in-band data only fills a blank. Exposed on PaneListEntry (`list-panes --json`) rather than PaneInfo, which crosses the plugin protobuf API we cannot regenerate; resolved in the screen thread where panes live rather than via route.rs's per-pane PTY round-trips. Values are sanitised to a bounded [A-Za-z0-9_.:/-] token at both entry points, because a purpose is never rendered -- it is matched against and pasted into consumers' shell and JSON pipelines. Verified end to end rather than by inspection: purposes from a layout appear in list-panes --json against the right pane ids, an agent-spawned pane tags itself, a declared purpose survives an in-band override attempt, all of it comes back after kill + resurrect, an unknown pane property is still a parse error, and a layout with no purpose leaves dump-layout unchanged.
Diffstat (limited to 'zellij-utils/src/kdl')
-rw-r--r--zellij-utils/src/kdl/kdl_layout_parser.rs8
1 files changed, 8 insertions, 0 deletions
diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs
index 643d268e..9e823849 100644
--- a/zellij-utils/src/kdl/kdl_layout_parser.rs
+++ b/zellij-utils/src/kdl/kdl_layout_parser.rs
@@ -111,6 +111,7 @@ impl<'a> KdlLayoutParser<'a> {
|| property_name == "arrangement"
|| property_name == "status_bar"
|| property_name == "status_bar_buttons"
+ || property_name == "purpose"
}
fn is_a_valid_floating_pane_property(&self, property_name: &str) -> bool {
property_name == "borderless"
@@ -131,6 +132,7 @@ impl<'a> KdlLayoutParser<'a> {
|| property_name == "contents_file"
|| property_name == "default_fg"
|| property_name == "default_bg"
+ || property_name == "purpose"
}
fn is_a_valid_tab_property(&self, property_name: &str) -> bool {
property_name == "focus"
@@ -577,6 +579,8 @@ impl<'a> KdlLayoutParser<'a> {
// Restored from a serialised session. Sanitised again on the way in: a
// layout file on disk is no more trustworthy than the escape sequence
// that produced it, and it may have been hand-edited since.
+ let purpose = kdl_get_string_property_or_child_value_with_error!(kdl_node, "purpose")
+ .and_then(|s| crate::input::layout::sanitize_pane_purpose(s));
let status_bar = kdl_get_string_property_or_child_value_with_error!(kdl_node, "status_bar")
.map(|s| crate::input::layout::sanitize_status_bar_text(s))
.filter(|s| !s.is_empty());
@@ -643,6 +647,7 @@ impl<'a> KdlLayoutParser<'a> {
default_bg,
status_bar,
status_bar_buttons,
+ purpose,
children_arrangement,
..Default::default()
})
@@ -668,6 +673,8 @@ impl<'a> KdlLayoutParser<'a> {
.map(|s| s.to_string());
let default_bg = kdl_get_string_property_or_child_value_with_error!(kdl_node, "default_bg")
.map(|s| s.to_string());
+ let purpose = kdl_get_string_property_or_child_value_with_error!(kdl_node, "purpose")
+ .and_then(|s| crate::input::layout::sanitize_pane_purpose(s));
self.assert_no_mixed_children_and_properties(kdl_node)?;
let pane_initial_contents = contents_file.and_then(|contents_file| {
self.file_name
@@ -690,6 +697,7 @@ impl<'a> KdlLayoutParser<'a> {
pane_initial_contents,
default_fg,
default_bg,
+ purpose,
..Default::default()
})
}