Skip to content

feat(cli): agents label their own tab, and the task row collects it (#358) - #359

Merged
simion merged 2 commits into
simion:mainfrom
GabrielDumbrava:feat/tab-props
Oct 4, 2026
Merged

simion merged 2 commits into
simion:mainfrom
GabrielDumbrava:feat/tab-props

Conversation

@GabrielDumbrava

Copy link
Copy Markdown
Contributor

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 bare termic prop lists the task's properties, termic prop <key> one key.
    --tab targets another tab (the usual id / index / title selector, reaching any strip tab like tab close), --task / --project another task.
  • MCP task_prop does the same (set, clear, list) and defaults to the caller's task, like the CLI.
  • list --json and status --json carry the collected view (props on the task) and each tab's own (props on each tab).

Sidebar

  • Task row, after the name on the left (never in the badge/kebab slot): 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.
    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: value with the tab it came from.
  • Each tab's row (task expanded) shows only what that tab set.
  • The sidebar filter (Sidebar: per-project task filter (text + notifications bell) #324) matches property values, so typing ABC-2 finds the task working on it.
  • Nothing changes for tasks without properties, and nothing is shown in compact mode.

How

  • Properties belong to the tab, so agents in one task never overwrite each other.
  • TERMIC_TAB_ID is injected into every agent PTY, on the host and through the env overlay docker::build_spec forwards into the container, so termic prop knows the caller's tab.
    Only agent PTYs get it (the aux terminal has no tab); it is reserved against extra named ports.
  • The webview store is the only writer (setTabProp, reached by a tab_props RPC).
    persisted_tabs is rewritten whole by syncDurableTabs, 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).
  • Persisted with the tab (props on PersistedTab, 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 close rule.
  • One collect rule, lib/tabProps.ts collectTaskProps: the sidebar renders it and cliAgentState pushes it to Rust for list / status, so the CLI and the window cannot disagree.
  • 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, used by the CLI before it touches the socket, the server and the MCP tool.
  • Wire protocol v18 (v17 was taken upstream); an older app answers "rerun your command".

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 gets TERMIC_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).
  • e2e 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 gains task_prop.
  • After rebasing on main at 6143e38: cargo test 1235 + 81 + 21 pass; cli.e2e.ts 44/44, mcp.e2e.ts 20/20.
  • npm test: the 18 failures are the same 18 that fail on main (profileScope, taskLabel, bottomTerminal.integration, pr, usageUnknownDismissed).
  • Full make e2e before the rebase: the files that failed were checked one by one; credentials and terminal-find fail identically on main, and the rest pass run on their own (the agent.e2e.ts IIP image case passed 3 runs out of 3).

tools_list_size_is_a_conscious_diff moves from 17800 to 18400 bytes for task_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, the TERMIC_CLI_HELP hint agents get, docs/e2e-coverage.md.

🤖 Generated with Claude Code

…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>
@GabrielDumbrava

Copy link
Copy Markdown
Contributor Author
SCR-20261004-kdfw SCR-20261004-jufu

…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 simion 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.

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.

@simion
simion merged commit 03f0ef7 into simion:main Oct 4, 2026
7 checks passed
@simion

simion commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Merged — thanks for this one, Gabriel. termic prop is exactly the kind of small, well-scoped primitive that makes the agent-tab story click into place. Appreciate the contribution.

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.

Tab properties: agents set key/value properties on their tab from the CLI and MCP, collected on the task's sidebar row

2 participants