Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.8.2

- Replaced the large CX Deck iTerm badge with compact native session-title
presentation.
- Made CX Deck scrollback timestamps opt-in by default while preserving explicit
stored preferences.
- Added natural forwarding of bare Codex CLI flags, so `cx --yolo` and similar
interactive launches work directly.
- Preserved conversation identity, zmx runtime generations, workspace behavior,
and resume semantics.

## 0.8.1

- Added exact native iTerm workspace capture and ordered split-tree restoration;
Expand Down
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ multiplexer, a replacement for Codex, or an agent messaging system.
- Closing a terminal pane does not kill Codex.
- Exact Codex UUIDs prevent fuzzy or accidental resume.
- Native iTerm2 windows, tabs, and splits stay the presentation layer.
- Persistent pane badges keep long conversation names visible in narrow layouts.
- Native iTerm2 timestamps show when scrollback lines were last modified.
- Native iTerm2 session titles provide compact conversation labels.
- Optional native iTerm2 timestamps show when scrollback lines were last modified.
- Groups, pins, display names, workspaces, and layouts stay local and private.

## How it works
Expand Down Expand Up @@ -57,8 +57,8 @@ cx
The installer checks prerequisites and installs CX Deck under
`~/.local/share/cxdeck`. It does not install zmx, restart Codex, or modify a
running zmx session. It creates only the `CX Deck` iTerm dynamic profile, which
inherits the user's default profile and supplies CX Deck-scoped badges and
timestamps.
inherits the user's default profile and supplies the CX Deck-scoped timestamp
preference.

To update an existing checkout:

Expand All @@ -71,8 +71,10 @@ git pull --ff-only

```zsh
cx # start a persistent Codex session here
cx --yolo # start a persistent Codex session in YOLO mode
cx "Model Evaluation" # start or reuse an exact display name
cx new --split # new native iTerm2 split
cx new --split -- --yolo # explicit split with Codex launch flags
cx resume # select from saved and live conversations
cx status
cx dashboard
Expand All @@ -92,6 +94,7 @@ Closing the pane detaches the view. The Codex process remains in its zmx PTY.
## Core commands

```text
cx [CODEX_FLAGS...]
cx new [--split|--tab|--window] [--count N]
cx resume [--safe|--yolo] [--select EXACT_UUID ...] [--no-iterm]
cx focus EXACT_NAME
Expand Down Expand Up @@ -159,19 +162,26 @@ presentation only and never resume a saved conversation.

## Native iTerm experience

CX Deck-created views use a native iTerm2 session badge backed by the
`user.cxdeck_name` session variable, with native session-name metadata as a
supplement. `cx rename` updates verified current views without reconnecting
Codex. At very narrow widths
iTerm2 may visually truncate presentation, while the untruncated CX Deck name
remains in the native session variable and session metadata.
CX Deck supplies each view's native iTerm2 session title. `cx rename` updates
verified current views without reconnecting Codex. To show compact labels for
split panes, enable this user-owned iTerm2 preference:

Native iTerm2 scrollback timestamps are enabled by default only in the CX Deck
profile. Toggle CX Deck views with:
```text
iTerm2 Settings
→ Appearance
→ Panes
→ Show per-pane title bar with split panes
```

CX Deck does not change that global preference and does not draw labels inside
terminal output.

Native iTerm2 scrollback timestamps are off by default. Enable them for CX Deck
views with:

```zsh
cx config timestamps off
cx config timestamps on
cx config timestamps off
```

No timestamp text is inserted into Codex output. CX Deck does not configure
Expand Down
5 changes: 4 additions & 1 deletion agent_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def _prepare_current_view(display_name):
import cx_iterm
try:
cx_iterm.prepare_current_view(
display_name, Store().preference('timestamps', True))
display_name, Store().preference('timestamps', False))
except (RuntimeError, OSError) as exc:
print('cx: presentation warning: ' + str(exc) +
'. The Codex process and zmx generation are unchanged.', file=sys.stderr)
Expand Down Expand Up @@ -380,6 +380,7 @@ def doctor():

HELP = f"""CX Deck {VERSION}: persistent Codex sessions with a native terminal experience
codex / cx / cx new new persistent Codex session here
cx --yolo / cx --model M forward Codex flags into a managed new session
cx LABEL create or attach a named session
cx new --split|--tab|--window open native iTerm2 views
cx resume saved/live picker; cold resume defaults to YOLO
Expand Down Expand Up @@ -407,6 +408,8 @@ def main(argv=None):
if not argv:
start_agent([])
return 0
if argv[0].startswith('-'):
return run_codex(argv)
command, rest = argv[0], argv[1:]
if command in ("resume", "fork"):
return run_codex([command, *rest])
Expand Down
2 changes: 1 addition & 1 deletion console_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
cx views status read-only runtime/presentation health
cx views rebuild [--workspace NAME] create missing verified views only
cx views refresh refresh names on verified existing iTerm views
cx config timestamps on|off native iTerm scrollback timestamps (default on)
cx config timestamps on|off native iTerm scrollback timestamps (default off)
cx upgrade status runtime compatibility for the installed cx version
cx upgrade apply defined safe upgrades; never restart implicitly

Expand Down
27 changes: 13 additions & 14 deletions cx_iterm.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@
set rows of childPane to 56
end try
end if
set childGuid to (unique id of childPane) as text
set childTTY to (tty of childPane) as text
set childGuid to (get unique id of childPane) as text
set childTTY to (get tty of childPane) as text
set paneGuids to {childGuid}
else if bestGuid is "" then
set targetWindow to missing value
Expand All @@ -218,8 +218,8 @@
set nextTab to (create tab with profile cxProfile command paneCommand)
set childPane to current session of nextTab
end tell
set childGuid to (unique id of childPane) as text
set childTTY to (tty of childPane) as text
set childGuid to (get unique id of childPane) as text
set childTTY to (get tty of childPane) as text
set paneGuids to {childGuid}
else
set splitPane to missing value
Expand Down Expand Up @@ -249,11 +249,11 @@
set childPane to (split horizontally with profile cxProfile command paneCommand)
end if
end tell
set childGuid to (unique id of childPane) as text
set childTTY to (tty of childPane) as text
set childGuid to (get unique id of childPane) as text
set childTTY to (get tty of childPane) as text
set end of paneGuids to childGuid
end if
if (profile name of childPane) is not cxProfile then error "iTerm2 did not create the pane with the CX Deck profile"
if (get profile name of childPane) is not cxProfile then error "iTerm2 did not create the pane with the CX Deck profile"
set name of childPane to paneName
set variable childPane named "user.cxdeck_name" to paneName
set output to output & childGuid & (ASCII character 9) & childTTY & linefeed
Expand Down Expand Up @@ -308,7 +308,7 @@ def parse_views(text):
return views


def profile_payload(timestamps=True):
def profile_payload(timestamps=False):
"""Return the minimal iTerm dynamic profile owned by CX Deck.

iTerm merges omitted keys from the current default profile. These overrides
Expand All @@ -318,7 +318,6 @@ def profile_payload(timestamps=True):
'Profiles': [{
'Name': PROFILE_NAME,
'Guid': PROFILE_GUID,
'Badge Text': r'\(user.cxdeck_name)',
'Timestamps Visible': bool(timestamps),
'Timestamps Style': 1,
}]
Expand All @@ -339,7 +338,7 @@ def _validate_profile_parents(path):
raise StateError(f'iTerm2 profile path is not an owned directory: {parent}')


def ensure_profile(timestamps=True, home=None):
def ensure_profile(timestamps=False, home=None):
"""Install/update only CX Deck's dynamic iTerm profile, atomically."""
path = _profile_path(home)
_validate_profile_parents(path)
Expand Down Expand Up @@ -416,7 +415,7 @@ def remove_profile(home=None):
return True


def prepare_current_view(display_name, timestamps=True, stream=None, environ=None,
def prepare_current_view(display_name, timestamps=False, stream=None, environ=None,
platform=None, home=None):
"""Apply the CX Deck profile to the current iTerm session before attach.

Expand All @@ -439,7 +438,7 @@ def prepare_current_view(display_name, timestamps=True, stream=None, environ=Non


class ITerm:
def configure(self, timestamps=True):
def configure(self, timestamps=False):
return ensure_profile(timestamps)

def call(self, *args):
Expand Down Expand Up @@ -543,7 +542,7 @@ def show(rows, b, store, gui=None, mode='window', anchor=None,
anchor = caller_tty() if anchor is None else anchor
configure = getattr(gui, 'configure', None)
if configure:
configure(store.preference('timestamps', True))
configure(store.preference('timestamps', False))
gui.preflight(mode, anchor)
with store.lock('views'):
data, procs = _presentation_snapshot(b)
Expand Down Expand Up @@ -603,7 +602,7 @@ def refresh(rows, b, store, gui=None):
gui = gui or ITerm()
configure = getattr(gui, 'configure', None)
if configure:
configure(store.preference('timestamps', True))
configure(store.preference('timestamps', False))
gui.preflight()
data, procs = _presentation_snapshot(b)
current = {r['_key']: r for r in data['sessions']}
Expand Down
2 changes: 1 addition & 1 deletion cx_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""CX Deck release identity."""

VERSION = "0.8.1"
VERSION = "0.8.2"
32 changes: 19 additions & 13 deletions docs/ITERM_PRESENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ CX Deck owns one iTerm
settings from the user's current default profile. The file overrides only:

```text
Badge Text = \(user.cxdeck_name)
Timestamps Visible = true|false
Timestamps Style = overlap
```

Views are created explicitly with that profile, so unrelated iTerm sessions and
the Default Profile are unchanged. The badge is iTerm's native overlay and does
not consume terminal rows. CX Deck also sets native session-name metadata, while
the attached program remains free to update its ordinary terminal title. The
badge and session-name metadata use the complete Store display name; iTerm may
visually shorten them when physical space is insufficient.
the Default Profile are unchanged. CX Deck sets the native iTerm session name,
while the attached program remains free to update its ordinary terminal title.
The compatibility variable `user.cxdeck_name` also carries the complete Store
display name, but the dynamic profile does not render it as a badge.

For compact labels, users may enable iTerm2 Settings → Appearance → Panes →
Show per-pane title bar with split panes. That is a global, user-owned iTerm2
preference; CX Deck never changes it. No title is drawn inside terminal output.

For `cx` and `cx new` in the caller's current pane, CX Deck selects its profile
and sets the same user variable with iTerm's documented OSC controls immediately
Expand All @@ -38,13 +40,16 @@ to all verified existing views. Neither operation writes to the PTY, sends input
attaches, detaches, or changes the zmx generation.

iTerm's `Timestamps Visible` session-profile property supplies per-line scrollback
times. `cx config timestamps on|off` updates only the CX Deck dynamic profile;
iTerm applies that profile update to existing views that use it as well as future
views. No prefix is inserted into terminal output, no scrollback database is
created, and no agent is reattached.
times. It defaults off when no preference is stored. `cx config timestamps
on|off` updates only the CX Deck dynamic profile and preserves an explicit
stored choice. A disposable iTerm2 3.7.0 probe confirmed that an already-open
session using a dynamic profile dropped a removed badge property within 0.5
seconds of the profile update. Existing CX Deck views therefore converge without
closing panes; future views use the same updated profile. No prefix is inserted
into terminal output, no scrollback database is created, and no agent is
reattached.

These mechanisms follow iTerm2's supported
[badge](https://iterm2.com/documentation-badges.html) and
These mechanisms follow iTerm2's supported session-title and
[session timestamp](https://iterm2.com/documentation-preferences-profiles-session.html)
features. CX Deck uses AppleScript only for supported session creation,
GUID/TTY inventory, focus, title, and user-variable operations; it does not use
Expand Down Expand Up @@ -87,6 +92,7 @@ links, terminal close, reattach, and child-exit cleanup. Common direct and zmx
behavior is equivalent; closing the zmx client preserves the child.

The disposable real-iTerm harness verifies native window, tab, and split
creation, unique badges/titles across five panes, narrow-pane resize, timestamps,
creation, unique native session titles across five panes, no badge override,
narrow-pane resize, timestamps,
inventory, focus, preferred-view reuse, pane disappearance, workspace reopen,
process preservation, and detach-key disabling.
2 changes: 1 addition & 1 deletion install.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def install(home, source, configure_iterm=None):
payloads = {name: (source / name).read_bytes() for name in FILES}
old_store = home / LEGACY_STATE_RELATIVE / 'workbench'
active_store = old_store if old_store.exists() else state_home(home) / 'workbench'
timestamps = Store(active_store).preference('timestamps', True)
timestamps = Store(active_store).preference('timestamps', False)
configure_iterm = sys.platform == 'darwin' if configure_iterm is None else configure_iterm
stamp = datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
backups = state_home(home) / 'backups'
Expand Down
14 changes: 7 additions & 7 deletions tests/iterm_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,6 @@ async def inspect(connection):
if session is None:
raise RuntimeError('Disposable iTerm session disappeared before profile verification')
properties = (await session.async_get_profile()).all_properties
if properties.get('Badge Text') != r'\(user.cxdeck_name)':
raise RuntimeError('Live iTerm session did not retain the CX Deck badge profile property')
if ('Timestamps Visible' not in properties or
bool(properties['Timestamps Visible']) != timestamps):
observed = {key: value for key, value in properties.items()
Expand All @@ -160,12 +158,14 @@ def main():
api_environment = {'HOME': str(Path.home()), **{
key: value for key, value in os.environ.items() if key.startswith('ITERM')}}
profile_path = cx_iterm._profile_path()
timestamps = True
timestamps = False
if profile_path.exists():
profile = json.loads(profile_path.read_text())['Profiles'][0]
if profile.get('Guid') != cx_iterm.PROFILE_GUID:
raise RuntimeError('Existing CX Deck profile is not owned by this installation')
timestamps = bool(profile.get('Timestamps Visible', True))
if 'Badge Text' in profile:
raise RuntimeError('CX Deck dynamic profile still contains a badge override')
timestamps = bool(profile.get('Timestamps Visible', False))
gui.configure(timestamps)
gui.preflight()
runtime = "/tmp/cxgi-" + uuid.uuid4().hex[:10]
Expand Down Expand Up @@ -225,7 +225,7 @@ def main():
presentation = gui.inspect(view)
if any((presentation['badge_name'] != item['display_name'],
presentation['session_name'] != item['display_name'])):
raise RuntimeError('Incorrect iTerm title/badge/profile assignment: ' + repr(presentation))
raise RuntimeError('Incorrect iTerm session-title metadata assignment: ' + repr(presentation))
profile_api_verified = verify_profile_api(
list(store.read()['views'].values()), timestamps, api_environment)
long_view = store.read()['views'][refreshed[0]['_key']]
Expand Down Expand Up @@ -280,8 +280,8 @@ def main():
print(json.dumps({"result": "PASS", "agents": 5, "zmx_version": "0.8.1",
"timestamps_enabled": timestamps,
"profile_api_verified": profile_api_verified,
"real_gui": True, "checks": ["native windows/splits/tabs", "five unique badges/titles",
"160/100/80/60-column name metadata", "native timestamp profile", "verified view reuse",
"real_gui": True, "checks": ["native windows/splits/tabs", "five unique native session titles",
"no CX Deck badge override", "160/100/80/60-column name metadata", "native timestamp profile", "verified view reuse",
"one preferred view", "direct attach focuses preferred view", "rename and workspace reopen", "close pane keeps PID",
"recreate missing view", "ZMX_NO_DETACH_KEY=1"]}))
finally:
Expand Down
Loading
Loading