Skip to content

Add session labels (get/set/unset/clear) and list --where filtering#185

Open
maxandersen wants to merge 2 commits into
neurosnap:mainfrom
maxandersen:tags
Open

Add session labels (get/set/unset/clear) and list --where filtering#185
maxandersen wants to merge 2 commits into
neurosnap:mainfrom
maxandersen:tags

Conversation

@maxandersen

Copy link
Copy Markdown

Implements #183 — session labels for discovery and filtering.

Fair warning: this is my first time writing Zig, so treat this as a "works but could use a more experienced eye on idioms" kind of PR. Happy to adjust anything that doesn't fit zmx conventions.

What this adds

Key-value labels that live on the session daemon, scoped to the session lifetime. Four new top-level commands:

zmx set mysession project=zmx env=dev
zmx get mysession
zmx unset mysession env
zmx clear mysession

Inside a session, . resolves via $ZMX_SESSION:

zmx set . status=busy

Labels show up automatically in zmx list output as extra tab-separated fields.

Filtering with --where

zmx list --where project=zmx          # exact match
zmx list --where start_dir=/code/zmx* # prefix match (trailing *)
zmx list --short --where name=build*  # just names, for scripting

Built-in fields name, start_dir, and cmd work as virtual labels in --where queries without setting anything — so zmx list --where start_dir=/Users/max/code* works out of the box. These fields are read-only; zmx set mysession name=foo is rejected.

Multiple --where flags combine with AND logic.

How it works

  • Five new IPC tags: LabelGet, LabelSet, LabelUnset, LabelClear, LabelData. Old daemons silently ignore unknown tags via the existing _ arm, so there's no backward compat issue.
  • zmx list sends both Info and LabelGet in a single batch on the same socket connection. If the daemon responds with LabelData, great. If not (old daemon), a 50ms poll times out and we move on. No extra connection per session.
  • When --where only filters on built-in fields, label fetching is skipped entirely — same speed as before.

What's included

  • IPC layer: new tags, probeSessionWithLabels for batched Info+Label fetch, roundTripForTag for single-session label ops
  • Daemon: in-memory StringHashMapUnmanaged label store with proper allocation/deallocation
  • CLI: get/set/unset/clear commands with aliases (g, z, un, cl)
  • list --where with exact and prefix matching
  • Reserved key protection (client-side and daemon-side)
  • Error messages instead of stack traces for missing sessions, old daemons, . without $ZMX_SESSION
  • Shell completions for all four shells (bash, zsh, fish, nu)
  • 15 BATS integration tests covering CRUD, reserved keys, error paths, and --where filtering
  • 6 unit tests for parseWhereExpr, whereMatch, and isReservedKey
  • README section and CHANGELOG entry

Build fix

The ghostty dependency was trying to build an iOS XCFramework on macOS without full Xcode. Added emit-lib-vt=true, emit-xcframework=false, emit-macos-app=false to all three b.dependency("ghostty", ...) calls. This might already be fixed upstream — just needed it to build locally with Command Line Tools only.

@maxandersen

Copy link
Copy Markdown
Author

I've been using this for a while now and it's really nice to have decoupled grouping and context sharing for terminals.

ATM I have to specify all labels again when new sessions created.

Default I think make sense to have default be to inherit labels but allow to set zmx_inherit_labels which can contain 0 to N comma separated labels.

This makes default children has same state but you tweak as you need.

@neurosnap

Copy link
Copy Markdown
Owner

How do you know when to inherit labels from another session?

@maxandersen

Copy link
Copy Markdown
Author

I mean like ZMX_SESSION_PREFIX is used to prefix, I would use the current ZMX_SESSION labels based on value/presence of ZMX_INHERIT_LABELS.

@maxandersen

Copy link
Copy Markdown
Author

note, my usecase was mainly just for when starting/running other zmx sessions from within another zmx started process.

I guess could also point to another session for "copying" but that wasn't on my mind in this case. wuold that be useful?

@neurosnap

Copy link
Copy Markdown
Owner

Do you think we actually need unset and clear? Couldn't unset just be set ""? I see the value in clear but I'm wondering how common it will be -- to the point where I'm wondering if we could get away without it?

@maxandersen

Copy link
Copy Markdown
Author

So emptystring means remove ? Yeah;
Probably works.

@maxandersen

Copy link
Copy Markdown
Author

Dropped unsetset key= (empty value) now removes the label. Keeps clear since there's no clean way to remove all labels without knowing every key.

@neurosnap neurosnap left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, excellent job on this PR. I think I would prefer to start simpler with labels: no prefix matching --where and no label inheritance. If we find there's too much friction and those feature are truly table-stakes we can re-evaluate.

What do you think? You don't need to make those changes, I'll do that myself, I just wanted to discuss with you before committing to it as I can be persuaded.

Thanks again!

Comment thread src/main.zig
if (idx == 0) return null;
const key = expr[0..idx];
const raw_value = expr[idx + 1 ..];
const is_prefix = raw_value.len > 0 and raw_value[raw_value.len - 1] == '*';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do you think we actually need --where key=value*? I feel like at a certain point we are just re-inventing other tools like grep.

Comment thread src/main.zig
Comment on lines +2170 to +2175
/// ponytail: inherit labels from parent session into a newly created daemon.
/// Uses $ZMX_SESSION if set, silently skips otherwise.
/// ZMX_INHERIT_LABELS controls filtering:
/// unset → inherit all
/// empty string → inherit nothing
/// "a,b" → inherit only keys a and b

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that we need an override for this feature feels like a smell. I'm not sure I want to include this into the initial version, I'd rather start simple and see where the friction exists before implementing inheritance with an env var override.

Comment thread src/main.zig
return help();
} else if (std.mem.eql(u8, arg, "--short")) {
short = true;
} else if (std.mem.startsWith(u8, arg, "--where")) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I'm thinking about it, I wonder if we should drop --where entirely and let users rely on pipes to filter based on labels

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How ? That would be some tab character matching extracting key=value ?

@maxandersen

Copy link
Copy Markdown
Author

I'll need to create and set those inherited labels but attach won't return the created session nor can I see the parent inside the created session.

How do you envision users do this if no inherit ?

@neurosnap

neurosnap commented Jul 18, 2026

Copy link
Copy Markdown
Owner

If we make get and set be symmetrical, we could do something like this:

zmx a first
zmx set . project=pico

zmx a second
zmx set . "$(zmx get first)"

Lemme know if that doesn't work for you.

@maxandersen

Copy link
Copy Markdown
Author

I think it make sense get can be piped into set.

Those are useful especially when creating a set of zmx attaches or some tool want control.

The issue with removing inherit labels is that you also need others to do these extra calls.

By having inherit labels (or sticky labels) any user or any tool can use zmx at any depth can share labels without having any understanding of zmx

So I would be sad and feel it makes it hard to be used :)

@neurosnap

Copy link
Copy Markdown
Owner

Fair enough. I'm working on your PR now and playing around with some different ideas. I'm going to start with the simplest version and then we can layer more complex workflows on top. I'll keep thinking about inheritance and if your solution is the best option. Thanks again!

@maxandersen

Copy link
Copy Markdown
Author

Cool. I've come to dislike the inheritance name/feel. It's more like what labels are sticky. It doesn't change the implementation but make for simpler explanation imo.

All labels are by default sticky when creating new sessions from an existing session. You can decide what labels you want to be sticky by setting ZMX_STICKY_LABELS to a comma separated list. Including the empty list which means nothing will stick.

Looking forward to where it will land :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants