feat(cli): agents label their own tab, and the task row collects it (#358) - #359
Merged
Merged
Conversation
…imion#358) `termic prop <key> <value>` puts a small key/value property on the caller's own tab (`termic prop ticket ABC-1`), `""` clears it and a bare `termic prop` lists them; `--tab` / `--task` aim elsewhere. MCP `task_prop` does the same. The task's sidebar row shows every tab's values after the name: one entry per key, keys in the order they were first set anywhere in the task, each key's distinct values joined with ", " in tab order, so two agents on two tickets read "ABC-1, ABC-2". Each tab's own row shows what it set, and the sidebar filter matches property values. `list --json` and `status --json` carry them. Properties belong to the tab, so agents in one task never overwrite each other. Every agent PTY now gets `TERMIC_TAB_ID`, on the host and through the env overlay in Docker, so the CLI knows which tab is the caller's. The webview store is the only writer: it applies one write at a time and persists the list with the tab, so a Rust-side write (which the next `task_set_tabs` would overwrite) is never needed. Values come from agents, so they are validated, not escaped: keys `[a-z0-9][a-z0-9_-]*` up to 32, values one line up to 40, 8 keys per tab, one rule in termic-proto shared by the CLI, server and MCP. An unchanged value writes nothing. Protocol v18. Closes simion#358 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
Author
…n#358) Windows gives the main thread 1 MB, and building the clap command tree for `help --json` already sat at that edge: a debug CLI built from main overflows at 900 KB. The `prop` verb pushed it past 1 MB, so the Windows e2e job died in `termic-cli help --json` with "thread 'main' has overflowed its stack". The CLI now runs on a thread with the 8 MB macOS and Linux give their main thread. Reproduced and verified on a Mac with `ulimit -s 1024`: fails before, passes down to 256 KB after. Also names `prop` in `termic --help`'s agent summary and quick start, and `task_prop` in the MCP server instructions. Refs simion#358 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
simion
approved these changes
Oct 4, 2026
simion
left a comment
Owner
There was a problem hiding this comment.
Reviewed the termic prop / MCP task_prop implementation: the TS/Rust validation rule is genuinely shared, the store remains the sole writer, TERMIC_TAB_ID is correctly scoped to agent PTYs, and the v18 negotiation degrades gracefully against older clients. No blocking issues found.
Owner
|
Merged — thanks for this one, Gabriel. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Closes #358.
What
Agents label their own tab with small key/value properties, and the task's sidebar row collects them.
termic prop <key> <value>sets a property on the caller's OWN tab (termic prop ticket ABC-1).termic prop <key> ""clears it, a baretermic proplists the task's properties,termic prop <key>one key.--tabtargets another tab (the usual id / index / title selector, reaching any strip tab liketab close),--task/--projectanother task.task_propdoes the same (set, clear, list) and defaults to the caller's task, like the CLI.list --jsonandstatus --jsoncarry the collected view (propson the task) and each tab's own (propson each tab).Sidebar
,in tab order.Two agents on two tickets read
ABC-1, ABC-2; with a status too,ABC-1, ABC-2 · review.An update keeps its key in place. The row's tooltip lists every
key: valuewith the tab it came from.ABC-2finds the task working on it.How
TERMIC_TAB_IDis injected into every agent PTY, on the host and through the env overlaydocker::build_specforwards into the container, sotermic propknows the caller's tab.Only agent PTYs get it (the aux terminal has no tab); it is reserved against extra named ports.
setTabProp, reached by atab_propsRPC).persisted_tabsis rewritten whole bysyncDurableTabs, so a Rust-side write would be overwritten by the next sync; the store applying one write at a time is what makes a single key atomic.An unchanged value writes nothing (bear trap 8).
propsonPersistedTab, each entry with the time its key was first set), so durable agent tabs keep them across a relaunch.Closing a tab drops them; writing into a task that is not open is refused, the
tab closerule.lib/tabProps.ts collectTaskProps: the sidebar renders it andcliAgentStatepushes it to Rust forlist/status, so the CLI and the window cannot disagree.[a-z0-9][a-z0-9_-]*up to 32, values one line up to 40, 8 keys per tab.One rule in
termic-proto, used by the CLI before it touches the socket, the server and the MCP tool.Tests
termic-proto(limits),termic-cli(parsing, pre-socket guard),cli_server.rs(routing to the resolved tab by every selector, list and key filter, refusals before any RPC, error mapping),mcp_server.rs(task_prop),lib.rs(only an agent PTY getsTERMIC_TAB_ID).lib/tabProps.test.ts(validation, apply, collect order and dedup),store/cliTabProps.integration.test.ts(the real handler on the real store: per-tab writes, persistence, no-op, clear, typed errors, reading an unopened task),taskFilter.test.ts(property values match).cli.e2e.ts: 6 new cases over the real socket and the real CLI binary with$TERMIC_TAB_ID: the task row and tab rows, key order and in-place updates,status --json, refusals and clear, the filter.mcp.e2e.ts's pinned tool list gainstask_prop.mainat 6143e38:cargo test1235 + 81 + 21 pass;cli.e2e.ts44/44,mcp.e2e.ts20/20.npm test: the 18 failures are the same 18 that fail onmain(profileScope,taskLabel,bottomTerminal.integration,pr,usageUnknownDismissed).make e2ebefore the rebase: the files that failed were checked one by one;credentialsandterminal-findfail identically onmain, and the rest pass run on their own (theagent.e2e.tsIIP image case passed 3 runs out of 3).tools_list_size_is_a_conscious_diffmoves from 17800 to 18400 bytes fortask_prop, with the reason next to the number.Manual test
The author tested it by hand in the dev app and reports it works.
Docs:
docs/data-model.md(the tab property entity),docs/cli-agent-instructions.md, theTERMIC_CLI_HELPhint agents get,docs/e2e-coverage.md.🤖 Generated with Claude Code