mixhub-cli 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mixhub_cli-0.2.2/.github/assets/terminal.png +0 -0
- mixhub_cli-0.2.2/.github/assets/web-dark.png +0 -0
- mixhub_cli-0.2.2/.github/assets/web-light.png +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/PKG-INFO +16 -14
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/README.md +16 -14
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/__init__.py +1 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/main.py +3 -1
- mixhub_cli-0.2.2/mixhub/core/cli_version.py +75 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/discovery.py +42 -11
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/send.py +3 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/session_filters.py +49 -33
- mixhub_cli-0.2.2/mixhub/hub/shell.py +85 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/summary.py +9 -4
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/ops/doctor.py +37 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/classify.py +2 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/patterns.py +4 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/quota.py +7 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/statusline.py +16 -8
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/headers.py +1 -1
- mixhub_cli-0.2.2/mixhub/tmux/home_shell.py +141 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/new_sessions.py +10 -3
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/onexit.py +48 -38
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/pool.py +2 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/session_state.py +19 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/workspace.py +2 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/badges.py +2 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/style.py +6 -6
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/drafts.py +35 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/actions.py +27 -8
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/colors.py +6 -6
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/draw.py +7 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/loop.py +1 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/selection_sync.py +25 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/sidebar.py +28 -11
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/quota_table.py +1 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/static/app.js +2 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/static/index.html +2 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/static/style.css +4 -4
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/server.py +1 -1
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_headless_and_hibernate_state.py +14 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_e2e_ux_audit.py +26 -12
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_hub.py +122 -2
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_probe.py +26 -0
- mixhub_cli-0.2.2/tests/test_session_hygiene.py +316 -0
- mixhub_cli-0.2.2/tests/test_sidebar_pinned_order.py +22 -0
- mixhub_cli-0.2.1/.github/assets/web-dark.png +0 -0
- mixhub_cli-0.2.1/.github/assets/web-light.png +0 -0
- mixhub_cli-0.2.1/mixhub/hub/shell.py +0 -49
- mixhub_cli-0.2.1/mixhub/tmux/home_shell.py +0 -62
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/.github/workflows/publish.yml +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/.github/workflows/test.yml +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/.gitignore +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/LICENSE +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/Preferences.example +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/bin/hub +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/bin/mixhub +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/install.sh +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/__main__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/base.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/cli.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/config.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/hooks.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/manager.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/slack.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/telegram.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/text.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/channels/webhook.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/daemon.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/sessions.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/tools.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/update.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/cli/workspace.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/browser.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/env.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/launch.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/paths.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/prefs.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/state.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/core/textwidth.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/__main__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/cursor.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/debuglog.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/main.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/mouse.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/patterns.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/prefs.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/stream.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/filters/agy_claude/vscreen.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/bundle.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/contract.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/disposition.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/extract.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/relay.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/resume.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/handoff/text.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/actions.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/constants.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/hub.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/index.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/integrations.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/lifecycle.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/listing.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/metrics.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/opencode.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/pane_probe.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/processes.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/schema.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/sleep_blockers.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/text.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/titles.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/transcript.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/util.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/hub/working_set.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/en.json +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/ja.json +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/i18n/zh.json +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/ops/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/capture.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/menus.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/rules.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/probe/text.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/classify.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/guard.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/identity.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/proc/tree.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/claude.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/deepseek.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/jsonl.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/launch.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/native_titles.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/preferences.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/presets.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/readiness.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/resume.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/providers/title_overrides.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/blocks.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/sync.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/rules/trust.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/commands.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/gateway.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tmux/outer.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/__main__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/app.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/caffeinate.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/canvas.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/events.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/geometry.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/hitmap.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/input.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/loop.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/core/widget.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/diag.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/attention.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/evq.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/footer.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/header.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/home.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/layout.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/mouse.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/navigation.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/popovers.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/prompt.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/reader.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/refresh.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/relay.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/hubtui/wordmark_row.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/paths.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/quota.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/selection.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/settings.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/sidebar_rows.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/web.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/chip.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/divider.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/header_bar.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/popover.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/preview_bar.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/sidebar_row.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/toast.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/tui/widgets/wordmark.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/__init__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/__main__.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/handler.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/mixhub/web/server.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/pyproject.toml +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/scripts/demo_seed.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/browser_check.mjs +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/README.md +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/agy_claude_filter.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/channels.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/handoff.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/mixhub_cli.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/probe.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/proc_guard.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/providers.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/rules_sync.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/tui.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/compat/wordmark.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/conftest.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/fixtures/agy_1_4_7_altscreen_typed_120x40.bin +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/fixtures/agy_1_4_7_altscreen_wrap_paste_40x30.bin +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/mcp_client.mjs +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_filter_mouse.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_filter_package.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_agy_headless_continued.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_auto_sleep_pref.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_bg_proc_scan.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_caffeinate.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_channels_config.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_consumer_checks.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_demo_seed.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_env_names.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_first_run.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_handoff_hold_and_fork.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_harness_presets.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_hub_providers_layout.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_hubpool_duplicate_windows.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_i18n.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_launch_forms.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_modular_and_trust.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_modular_channels_handoff.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_no_agent_file_deletion.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_notify_hooks.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_opencode.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_oss_desensitize_and_cc_ds.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_own_tmux_server.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_package_import_rule.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_packaging.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_platform_support.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_preferences_lookup.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_proc_guard.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_providers_live.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_rules_sync.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_schema_resilience.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_server.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_state_root.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_statusline_command.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_subagent_filter.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_telegram_bot_api_send.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_test_isolation.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_tui_core_primitives.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_tui_module_layout.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_update.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_web_navigation.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_windows_compat.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_wordmark.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_working_set_removal_log.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/test_zoom_toggle_and_badge.py +0 -0
- {mixhub_cli-0.2.1 → mixhub_cli-0.2.2}/tests/webui_composer.test.mjs +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mixhub-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A terminal sidebar for your Claude Code, Codex, Gemini and OpenCode sessions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ShinoKana/mixhub
|
|
6
6
|
Project-URL: Repository, https://github.com/ShinoKana/mixhub.git
|
|
@@ -64,17 +64,19 @@ What it doesn't do: it doesn't create worktrees or touch git, doesn't patch the
|
|
|
64
64
|
|
|
65
65
|
## Similar tools
|
|
66
66
|
|
|
67
|
-
| |
|
|
68
|
-
| :--- | :--- | :--- | :--- | :--- |
|
|
69
|
-
| MixHub |
|
|
70
|
-
| [
|
|
71
|
-
| [cmux](https://github.com/manaflow-ai/cmux) |
|
|
72
|
-
| [claude-squad](https://github.com/smtg-ai/claude-squad) |
|
|
73
|
-
| [ccmanager](https://github.com/kbwo/ccmanager) |
|
|
74
|
-
| [agent-deck](https://github.com/asheshgoplani/agent-deck) |
|
|
75
|
-
| [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) |
|
|
67
|
+
| | Other agents | Runs in | Status | Outside sessions |
|
|
68
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
69
|
+
| MixHub | Codex, Gemini, OpenCode | tmux | session files | listed |
|
|
70
|
+
| [Agent View](https://code.claude.com/docs/en/agent-view) | none | `claude` CLI | yes | after `/bg` |
|
|
71
|
+
| [cmux](https://github.com/manaflow-ai/cmux) | Codex, Gemini, OpenCode, … | macOS app | hooks / OSC | – |
|
|
72
|
+
| [claude-squad](https://github.com/smtg-ai/claude-squad) | Codex, Gemini, Aider | tmux | – | no |
|
|
73
|
+
| [ccmanager](https://github.com/kbwo/ccmanager) | Codex, Gemini, OpenCode, … | any terminal | yes | – |
|
|
74
|
+
| [agent-deck](https://github.com/asheshgoplani/agent-deck) | Codex, Gemini, OpenCode, … | tmux | yes | no |
|
|
75
|
+
| [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) | Codex, Devin | your tmux | hooks | if in tmux |
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
– means the project's docs don't say.
|
|
78
|
+
|
|
79
|
+
If you only use Claude Code, Agent View is built in. If you want a git worktree per task, claude-squad and ccmanager make one for you (agent-deck optionally); MixHub doesn't touch git.
|
|
78
80
|
|
|
79
81
|
## Install
|
|
80
82
|
|
|
@@ -123,7 +125,7 @@ mixhub update --check # only say whether a newer version exists
|
|
|
123
125
|
| Codex | `~/.codex/sessions/` rollouts + state DB | `codex resume <id>` |
|
|
124
126
|
| OpenCode | `opencode.db` (read-only) | `opencode -s <id>` |
|
|
125
127
|
|
|
126
|
-
`✎ New session` can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
|
|
128
|
+
`✎ New session` opens the new session in the working directory of the session selected in the sidebar, so to start in a project folder, click one of that folder's sessions first. With nothing selected, including the `LAUNCH` buttons on Home, it uses the folder you ran `mixhub` from. It can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
|
|
127
129
|
|
|
128
130
|
## The workspace
|
|
129
131
|
|
|
@@ -131,7 +133,7 @@ mixhub update --check # only say whether a newer version exists
|
|
|
131
133
|
|
|
132
134
|
**Right pane.** Clicking a session that is running in MixHub switches to its live CLI window. Clicking any other session shows a read-only transcript preview with a turn minimap; `[ Resume ↵ ]` starts the CLI in place, and `[ Go to Tab ]` jumps to a session that is running in another Terminal tab. Each live window has a two-row header with the working directory, session id and title.
|
|
133
135
|
|
|
134
|
-
**Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L).
|
|
136
|
+
**Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L). An agent started there (`claude`, `codex`, `opencode`, `agy`) moves to its own window in `Active`, the same as `✎ New session`, and Home gets a fresh shell; one-shot runs like `claude -p` stay in the shell.
|
|
135
137
|
|
|
136
138
|
**Footer.** Used `5h` and weekly quota with reset times for each agent that has at least one session on this machine, plus CPU and memory. Codex quota comes from its own session files. For exact Claude Code and Gemini (AGY) numbers, point their status line at MixHub, which records the quota each agent reports and prints a short status line back:
|
|
137
139
|
|
|
@@ -265,7 +267,7 @@ Everything works without configuration. Optional settings live in a JSON file na
|
|
|
265
267
|
| `MIXHUB_NO_OPEN=1` | `mixhub web` starts the daemon without opening a browser |
|
|
266
268
|
| `MIXHUB_NO_UPDATE_CHECK=1` | No daily update check |
|
|
267
269
|
|
|
268
|
-
`host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones.
|
|
270
|
+
`host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones. Sessions another agent started are hidden by what their CLI records (subagents, headless runs, Codex tasks whose `originator` is Claude Code). For older sessions without such a record, prompts that your own automation sends can be hidden with `data/local_rules.json` (`automated_prefixes`, `automated_substrings`, and `codex_automated_prefixes` for Codex only); `mixhub doctor --hidden` lists every session a text rule hides. Notes your own tools type into an agent on another agent's behalf go in `relayed_prefixes`: they never count as you having used a session, but don't hide one either.
|
|
269
271
|
|
|
270
272
|
## Architecture
|
|
271
273
|
|
|
@@ -37,17 +37,19 @@ What it doesn't do: it doesn't create worktrees or touch git, doesn't patch the
|
|
|
37
37
|
|
|
38
38
|
## Similar tools
|
|
39
39
|
|
|
40
|
-
| |
|
|
41
|
-
| :--- | :--- | :--- | :--- | :--- |
|
|
42
|
-
| MixHub |
|
|
43
|
-
| [
|
|
44
|
-
| [cmux](https://github.com/manaflow-ai/cmux) |
|
|
45
|
-
| [claude-squad](https://github.com/smtg-ai/claude-squad) |
|
|
46
|
-
| [ccmanager](https://github.com/kbwo/ccmanager) |
|
|
47
|
-
| [agent-deck](https://github.com/asheshgoplani/agent-deck) |
|
|
48
|
-
| [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) |
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
| | Other agents | Runs in | Status | Outside sessions |
|
|
41
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
42
|
+
| MixHub | Codex, Gemini, OpenCode | tmux | session files | listed |
|
|
43
|
+
| [Agent View](https://code.claude.com/docs/en/agent-view) | none | `claude` CLI | yes | after `/bg` |
|
|
44
|
+
| [cmux](https://github.com/manaflow-ai/cmux) | Codex, Gemini, OpenCode, … | macOS app | hooks / OSC | – |
|
|
45
|
+
| [claude-squad](https://github.com/smtg-ai/claude-squad) | Codex, Gemini, Aider | tmux | – | no |
|
|
46
|
+
| [ccmanager](https://github.com/kbwo/ccmanager) | Codex, Gemini, OpenCode, … | any terminal | yes | – |
|
|
47
|
+
| [agent-deck](https://github.com/asheshgoplani/agent-deck) | Codex, Gemini, OpenCode, … | tmux | yes | no |
|
|
48
|
+
| [tmux-agent-status](https://github.com/samleeney/tmux-agent-status) | Codex, Devin | your tmux | hooks | if in tmux |
|
|
49
|
+
|
|
50
|
+
– means the project's docs don't say.
|
|
51
|
+
|
|
52
|
+
If you only use Claude Code, Agent View is built in. If you want a git worktree per task, claude-squad and ccmanager make one for you (agent-deck optionally); MixHub doesn't touch git.
|
|
51
53
|
|
|
52
54
|
## Install
|
|
53
55
|
|
|
@@ -96,7 +98,7 @@ mixhub update --check # only say whether a newer version exists
|
|
|
96
98
|
| Codex | `~/.codex/sessions/` rollouts + state DB | `codex resume <id>` |
|
|
97
99
|
| OpenCode | `opencode.db` (read-only) | `opencode -s <id>` |
|
|
98
100
|
|
|
99
|
-
`✎ New session` can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
|
|
101
|
+
`✎ New session` opens the new session in the working directory of the session selected in the sidebar, so to start in a project folder, click one of that folder's sessions first. With nothing selected, including the `LAUNCH` buttons on Home, it uses the folder you ran `mixhub` from. It can also start Claude Code with another model behind it (DeepSeek, Kimi, Qwen and GLM presets); the badge shows which model a session uses.
|
|
100
102
|
|
|
101
103
|
## The workspace
|
|
102
104
|
|
|
@@ -104,7 +106,7 @@ mixhub update --check # only say whether a newer version exists
|
|
|
104
106
|
|
|
105
107
|
**Right pane.** Clicking a session that is running in MixHub switches to its live CLI window. Clicking any other session shows a read-only transcript preview with a turn minimap; `[ Resume ↵ ]` starts the CLI in place, and `[ Go to Tab ]` jumps to a session that is running in another Terminal tab. Each live window has a two-row header with the working directory, session id and title.
|
|
106
108
|
|
|
107
|
-
**Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L).
|
|
109
|
+
**Home.** With nothing selected, the right pane holds a plain login shell under the preview, for quick commands without leaving the workspace. Clear it the usual way (`clear` or Ctrl+L). An agent started there (`claude`, `codex`, `opencode`, `agy`) moves to its own window in `Active`, the same as `✎ New session`, and Home gets a fresh shell; one-shot runs like `claude -p` stay in the shell.
|
|
108
110
|
|
|
109
111
|
**Footer.** Used `5h` and weekly quota with reset times for each agent that has at least one session on this machine, plus CPU and memory. Codex quota comes from its own session files. For exact Claude Code and Gemini (AGY) numbers, point their status line at MixHub, which records the quota each agent reports and prints a short status line back:
|
|
110
112
|
|
|
@@ -238,7 +240,7 @@ Everything works without configuration. Optional settings live in a JSON file na
|
|
|
238
240
|
| `MIXHUB_NO_OPEN=1` | `mixhub web` starts the daemon without opening a browser |
|
|
239
241
|
| `MIXHUB_NO_UPDATE_CHECK=1` | No daily update check |
|
|
240
242
|
|
|
241
|
-
`host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones.
|
|
243
|
+
`host_label` is the machine name shown in the web workbench (the hostname by default). `dispatch_scripts` names your own wrapper scripts that start agents headlessly, so their sessions are not mistaken for interactive ones. Sessions another agent started are hidden by what their CLI records (subagents, headless runs, Codex tasks whose `originator` is Claude Code). For older sessions without such a record, prompts that your own automation sends can be hidden with `data/local_rules.json` (`automated_prefixes`, `automated_substrings`, and `codex_automated_prefixes` for Codex only); `mixhub doctor --hidden` lists every session a text rule hides. Notes your own tools type into an agent on another agent's behalf go in `relayed_prefixes`: they never count as you having used a session, but don't hide one either.
|
|
242
244
|
|
|
243
245
|
## Architecture
|
|
244
246
|
|
|
@@ -30,7 +30,9 @@ USAGE = """Usage: mixhub [command]
|
|
|
30
30
|
mixhub web open the web workbench (starts the local daemon)
|
|
31
31
|
mixhub start|stop|status
|
|
32
32
|
control the web daemon without opening a browser
|
|
33
|
-
mixhub doctor
|
|
33
|
+
mixhub doctor [--hidden]
|
|
34
|
+
check agents, tmux, channels and terminal settings;
|
|
35
|
+
--hidden lists sessions hidden only by a text rule
|
|
34
36
|
mixhub handoff SESSION_ID --to claude|codex|agy [--mode delegate|review] [--dry-run]
|
|
35
37
|
hand a session over to another agent
|
|
36
38
|
mixhub notify ... send a notification through the configured channels
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`mixhub.core.cli_version`: Installed Claude Code version against the version a live session started with.
|
|
3
|
+
|
|
4
|
+
Only a hint: a session on an older build keeps running it until it is restarted, and MixHub never restarts one
|
|
5
|
+
itself (a restart drops Remote Control, and Claude Code refuses `/restart` after a `cd`).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import glob
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
_VERSION_RE = re.compile(r"^\d+(\.\d+)+$")
|
|
17
|
+
_CACHE_SEC = 30.0
|
|
18
|
+
_installed_cache: dict[str, tuple[float, str]] = {}
|
|
19
|
+
_running_cache: dict[str, tuple[float, dict[str, str]]] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def installed_claude_version() -> str:
|
|
23
|
+
"""Version of the `claude` on PATH, read from the native installer's `versions/<x.y.z>` symlink target.
|
|
24
|
+
Empty when it can't be told without running the binary (npm installs, wrappers)."""
|
|
25
|
+
now = time.monotonic()
|
|
26
|
+
hit = _installed_cache.get("claude")
|
|
27
|
+
if hit and now - hit[0] < _CACHE_SEC:
|
|
28
|
+
return hit[1]
|
|
29
|
+
ver = ""
|
|
30
|
+
path = shutil.which("claude")
|
|
31
|
+
if path:
|
|
32
|
+
name = os.path.basename(os.path.realpath(path))
|
|
33
|
+
if _VERSION_RE.match(name):
|
|
34
|
+
ver = name
|
|
35
|
+
_installed_cache["claude"] = (now, ver)
|
|
36
|
+
return ver
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def running_claude_versions() -> dict[str, str]:
|
|
40
|
+
"""`{sessionId: version}` from the live processes' `~/.claude/sessions/<pid>.json`, for a `--header` pane that
|
|
41
|
+
has no discovery of its own."""
|
|
42
|
+
now = time.monotonic()
|
|
43
|
+
hit = _running_cache.get("claude")
|
|
44
|
+
if hit and now - hit[0] < _CACHE_SEC:
|
|
45
|
+
return hit[1]
|
|
46
|
+
found: dict[str, str] = {}
|
|
47
|
+
for fp in glob.glob(os.path.join(os.path.expanduser("~"), ".claude", "sessions", "*.json")):
|
|
48
|
+
try:
|
|
49
|
+
with open(fp, encoding="utf-8") as f:
|
|
50
|
+
info = json.load(f)
|
|
51
|
+
pid, sid, ver = int(info["pid"]), str(info["sessionId"]), str(info.get("version") or "")
|
|
52
|
+
os.kill(pid, 0)
|
|
53
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
54
|
+
continue
|
|
55
|
+
if ver:
|
|
56
|
+
found[sid] = ver
|
|
57
|
+
_running_cache["claude"] = (now, found)
|
|
58
|
+
return found
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _as_tuple(ver: str) -> tuple[int, ...]:
|
|
62
|
+
return tuple(int(x) for x in ver.split("."))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def old_version_hint(s: dict) -> str:
|
|
66
|
+
"""`old 2.1.316 · 2.1.318 installed` for a Claude session started on an older build, else empty."""
|
|
67
|
+
if not s or s.get("tool") != "claude":
|
|
68
|
+
return ""
|
|
69
|
+
running = str(s.get("cliVersion") or running_claude_versions().get(str(s.get("id") or ""), ""))
|
|
70
|
+
if not _VERSION_RE.match(running):
|
|
71
|
+
return ""
|
|
72
|
+
installed = installed_claude_version()
|
|
73
|
+
if not installed or _as_tuple(running) >= _as_tuple(installed):
|
|
74
|
+
return ""
|
|
75
|
+
return f"old {running} · {installed} installed"
|
|
@@ -31,9 +31,9 @@ from mixhub.hub.schema import claude_is_headless_marker, codex_state_db_candidat
|
|
|
31
31
|
from mixhub.hub.session_filters import (
|
|
32
32
|
_load_local_rules,
|
|
33
33
|
_LOCAL_RULES_CACHE,
|
|
34
|
+
codex_automation_reason,
|
|
34
35
|
is_automated_or_subagent_text,
|
|
35
36
|
is_users_own_prompt,
|
|
36
|
-
is_codex_automated,
|
|
37
37
|
is_temp_session,
|
|
38
38
|
)
|
|
39
39
|
from mixhub.hub.shell import resume_command
|
|
@@ -46,6 +46,21 @@ from mixhub.proc.identity import get_boot_id
|
|
|
46
46
|
from mixhub.proc.tree import has_agent_ancestor
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def settle_parked_status(value: dict, jobs_dir: str) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
A Claude session that parked its turn into a background job (`parkedJobId`) keeps the `busy` it had at that
|
|
52
|
+
moment in its pid file. The work runs in the job, so the session is busy only while the job is still working.
|
|
53
|
+
"""
|
|
54
|
+
job_id = str(value.get("parkedJobId") or "")
|
|
55
|
+
if job_id and value.get("status") == "busy":
|
|
56
|
+
job = read_json(os.path.join(jobs_dir, job_id, "state.json"), {})
|
|
57
|
+
if not isinstance(job, dict) or job.get("state") != "working":
|
|
58
|
+
value["status"] = "idle"
|
|
59
|
+
# Its transcript also stops mid-turn at the hand-off, which would otherwise still read as working
|
|
60
|
+
value["_parkedDone"] = True
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
49
64
|
class DiscoveryMixin:
|
|
50
65
|
"""Discover sessions from every provider store and merge them with live processes and hub state."""
|
|
51
66
|
|
|
@@ -298,7 +313,7 @@ class DiscoveryMixin:
|
|
|
298
313
|
value["_mtime"] = os.stat(path).st_mtime
|
|
299
314
|
except OSError:
|
|
300
315
|
value["_mtime"] = 0
|
|
301
|
-
pid_states[int(value["pid"])] = value
|
|
316
|
+
pid_states[int(value["pid"])] = settle_parked_status(value, self.path(".claude/jobs"))
|
|
302
317
|
self.sessions.close()
|
|
303
318
|
self.sessions = sessions = SessionIndex()
|
|
304
319
|
|
|
@@ -548,7 +563,7 @@ class DiscoveryMixin:
|
|
|
548
563
|
# archived_sessions/ only resolves rows whose recorded rollout_path moved; it never adds new rows.
|
|
549
564
|
codex_session_files, codex_disk_paths = self._discover_codex_rollouts()
|
|
550
565
|
codex_disk_paths_set = set(codex_session_files) | set(codex_disk_paths.values())
|
|
551
|
-
codex_columns = ["id", "title", "cwd", "first_user_message", "updated_at", "created_at", "model", "reasoning_effort", "rollout_path", "agent_role", "source"]
|
|
566
|
+
codex_columns = ["id", "title", "cwd", "first_user_message", "updated_at", "created_at", "model", "reasoning_effort", "rollout_path", "agent_role", "source", "originator"]
|
|
552
567
|
known_codex_paths = set()
|
|
553
568
|
# Follow Codex's state DB schema bumps (state_5 -> state_6 ...): newest file that actually
|
|
554
569
|
# has a `threads` table wins; otherwise fall back to older candidates.
|
|
@@ -572,18 +587,20 @@ class DiscoveryMixin:
|
|
|
572
587
|
len(raw_first),
|
|
573
588
|
raw_first[:128],
|
|
574
589
|
csrc,
|
|
590
|
+
row.get("originator"),
|
|
575
591
|
row.get("agent_role"),
|
|
576
592
|
rules_mtime,
|
|
577
593
|
)
|
|
578
594
|
cmemo = self._codex_row_memo.get(rid)
|
|
579
595
|
if cmemo is not None and cmemo[0] == codex_sig:
|
|
580
|
-
parsed_title, parsed_first,
|
|
596
|
+
parsed_title, parsed_first, row_auto_by, row_up_ts = cmemo[1]
|
|
581
597
|
else:
|
|
582
598
|
parsed_title = snippet(raw_title, 200)
|
|
583
599
|
parsed_first = snippet(clean_prompt(raw_first, "codex"))
|
|
584
|
-
|
|
600
|
+
row_auto_by = "source" if row.get("agent_role") else codex_automation_reason(csrc, raw_title, raw_first, row.get("originator"))
|
|
585
601
|
row_up_ts = stamp(row.get("updated_at"))
|
|
586
|
-
self._codex_row_memo[rid] = (codex_sig, (parsed_title, parsed_first,
|
|
602
|
+
self._codex_row_memo[rid] = (codex_sig, (parsed_title, parsed_first, row_auto_by, row_up_ts))
|
|
603
|
+
row_is_auto = bool(row_auto_by)
|
|
587
604
|
path = row.get("rollout_path")
|
|
588
605
|
resolved_path = None
|
|
589
606
|
if path:
|
|
@@ -604,6 +621,8 @@ class DiscoveryMixin:
|
|
|
604
621
|
"firstPrompt": parsed_first,
|
|
605
622
|
"model": row.get("model"), "reasoningEffort": row.get("reasoning_effort"),
|
|
606
623
|
"automated": is_auto})
|
|
624
|
+
if row_auto_by:
|
|
625
|
+
s["automatedBy"] = row_auto_by
|
|
607
626
|
if resolved_path:
|
|
608
627
|
s["path"] = resolved_path
|
|
609
628
|
known_codex_paths.add(resolved_path)
|
|
@@ -853,9 +872,13 @@ class DiscoveryMixin:
|
|
|
853
872
|
s.update(isLive=True, synthetic=synthetic)
|
|
854
873
|
s["cwd"] = info.get("cwd") or opened.get("cwd") or s["cwd"]
|
|
855
874
|
s["status"] = info.get("status") or s.get("status", "unknown")
|
|
875
|
+
if info.get("_parkedDone"):
|
|
876
|
+
s["parkedDone"] = True
|
|
856
877
|
if tool == "claude" and info:
|
|
857
878
|
lock_ts = max(float(info.get("_mtime") or 0), stamp(info.get("updatedAt")), stamp(info.get("startedAt")))
|
|
858
879
|
s["lockMtime"] = lock_ts
|
|
880
|
+
if info.get("version"):
|
|
881
|
+
s["cliVersion"] = str(info["version"])
|
|
859
882
|
if lock_ts > 0 and not s.get("updatedAt"):
|
|
860
883
|
s["updatedAt"] = lock_ts
|
|
861
884
|
elif tool in active_locks and sid in active_locks[tool]:
|
|
@@ -952,6 +975,8 @@ class DiscoveryMixin:
|
|
|
952
975
|
s["isLive"] = True
|
|
953
976
|
lock_ts = max(float(info.get("_mtime") or 0), stamp(info.get("updatedAt")), stamp(info.get("startedAt")))
|
|
954
977
|
s["lockMtime"] = lock_ts
|
|
978
|
+
if info.get("version"):
|
|
979
|
+
s["cliVersion"] = str(info["version"])
|
|
955
980
|
if lock_ts > 0 and not s.get("updatedAt"):
|
|
956
981
|
s["updatedAt"] = lock_ts
|
|
957
982
|
if not any(pr.get("pid") == pid for pr in s["processes"]):
|
|
@@ -1060,6 +1085,8 @@ class DiscoveryMixin:
|
|
|
1060
1085
|
not_idle = bool(s.get("notFullyIdle") and not s.get("killed") and age < 1800)
|
|
1061
1086
|
if raw_status in ("working", "running", "busy") or ("running" in raw_status and age < 1800) or not_idle:
|
|
1062
1087
|
s["agentState"] = "working"
|
|
1088
|
+
elif s.get("parkedDone"):
|
|
1089
|
+
s["agentState"] = "done_idle"
|
|
1063
1090
|
elif not has_procs:
|
|
1064
1091
|
s["agentState"] = "done_idle"
|
|
1065
1092
|
if tstat in ("ball_in_model", "unclosed_tool"):
|
|
@@ -1174,10 +1201,9 @@ class DiscoveryMixin:
|
|
|
1174
1201
|
exit_rc = int(exit_info.get("rc", 0))
|
|
1175
1202
|
except (TypeError, ValueError, AttributeError):
|
|
1176
1203
|
exit_rc = 0
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if not signal_exit and exit_ts > 0.0 and exit_ts >= sess_ts - 5.0:
|
|
1204
|
+
# Only rc 0 is the user leaving (`/exit`); a signal or a CLI that failed to start is not
|
|
1205
|
+
user_exit = exit_rc == 0 and not (isinstance(exit_info, dict) and exit_info.get("signal_exit"))
|
|
1206
|
+
if user_exit and exit_ts > 0.0 and exit_ts >= sess_ts - 5.0:
|
|
1181
1207
|
s["isLive"] = False
|
|
1182
1208
|
s["inWorkingSet"] = False
|
|
1183
1209
|
s["agentState"] = "dormant"
|
|
@@ -1187,7 +1213,12 @@ class DiscoveryMixin:
|
|
|
1187
1213
|
wset_dirty = True
|
|
1188
1214
|
wset_removed.append((key, f"exited rc={exit_rc}"))
|
|
1189
1215
|
continue
|
|
1190
|
-
if s.get("isLive")
|
|
1216
|
+
if s.get("isLive") and not s.get("processes") and not s.get("resident") \
|
|
1217
|
+
and not int(s.get("activeSubagents") or 0) and key not in wset and s["id"] not in wset:
|
|
1218
|
+
# Live by a Codex / AGY lock alone, with no interactive process: a one-shot run another agent
|
|
1219
|
+
# started (`agy -p`, a Codex task). It shows in `Recent` and never enters `Active`.
|
|
1220
|
+
s["inWorkingSet"] = False
|
|
1221
|
+
elif s.get("isLive"):
|
|
1191
1222
|
s["inWorkingSet"] = True
|
|
1192
1223
|
if key not in wset:
|
|
1193
1224
|
wset.add(key)
|
|
@@ -7,6 +7,8 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import subprocess
|
|
9
9
|
|
|
10
|
+
from mixhub.hub.shell import resume_cwd
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
def find_controlled_tmux_pane(session: dict, tmux_lister=None) -> str:
|
|
12
14
|
"""
|
|
@@ -73,7 +75,7 @@ def send_to_session(
|
|
|
73
75
|
resolved_tool = session["tool"]
|
|
74
76
|
resolved_id = session["id"]
|
|
75
77
|
skey = f"{resolved_tool}:{resolved_id}"
|
|
76
|
-
cwd = session.get("cwd") or home
|
|
78
|
+
cwd = resume_cwd(session) if session.get("path") else (session.get("cwd") or home)
|
|
77
79
|
|
|
78
80
|
clean_files = [str(p).strip() for p in (attachments or []) if str(p).strip()]
|
|
79
81
|
prompt_parts = [str(text or "").strip()] if str(text or "").strip() else []
|
|
@@ -13,31 +13,36 @@ from mixhub.hub.titles import FALLBACK_TITLE_RE
|
|
|
13
13
|
from mixhub.hub.util import read_json
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def is_codex_automated(source=None, title="", first_msg=""):
|
|
16
|
+
def codex_automation_reason(source=None, title="", first_msg="", originator=None) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Why a Codex thread counts as automated, or "" when it doesn't. What Codex recorded about who started the thread
|
|
19
|
+
decides first (`originator`, `source`); text rules only cover threads older than those fields.
|
|
20
|
+
"""
|
|
21
|
+
# A task Claude Code dispatched to Codex (codex-companion); its result goes back to that Claude session
|
|
22
|
+
if originator == "Claude Code":
|
|
23
|
+
return "originator"
|
|
26
24
|
if isinstance(source, dict) or (isinstance(source, str) and (source == "exec" or source.startswith("{"))):
|
|
27
|
-
return
|
|
25
|
+
return "source"
|
|
28
26
|
t = (title or "").strip()
|
|
29
27
|
m = (first_msg or "").strip()
|
|
30
|
-
if "<task>" in m or "<task>" in t
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
if "<task>" in m or "<task>" in t:
|
|
29
|
+
return "text:<task>"
|
|
30
|
+
for prefix in ("Codex Companion Task:", "## Referenced ChatGPT conversation:"):
|
|
31
|
+
if m.startswith(prefix) or t.startswith(prefix):
|
|
32
|
+
return "text:" + prefix
|
|
33
|
+
for word in ("--help", "exit", "usage"):
|
|
34
|
+
if word in (m, t):
|
|
35
|
+
return "text:" + word
|
|
36
|
+
low = m.casefold()
|
|
37
|
+
for prefix in _load_local_rules().get("codex_automated_prefixes") or ():
|
|
38
|
+
if low.startswith(prefix.casefold()):
|
|
39
|
+
return "text:" + prefix
|
|
40
|
+
rule = automated_text_rule(t) or automated_text_rule(m)
|
|
41
|
+
return "text:" + rule if rule else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_codex_automated(source=None, title="", first_msg="", originator=None):
|
|
45
|
+
return bool(codex_automation_reason(source, title, first_msg, originator))
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
_DISCOVER_GEN: int = 0
|
|
@@ -113,7 +118,7 @@ def _load_local_rules() -> dict:
|
|
|
113
118
|
if not isinstance(raw, dict):
|
|
114
119
|
raw = {}
|
|
115
120
|
data = {}
|
|
116
|
-
for key in ("automated_prefixes", "automated_substrings", "relayed_prefixes"):
|
|
121
|
+
for key in ("automated_prefixes", "automated_substrings", "relayed_prefixes", "codex_automated_prefixes"):
|
|
117
122
|
vals = raw.get(key)
|
|
118
123
|
data[key] = [str(v) for v in vals if isinstance(v, str) and v.strip()] if isinstance(vals, list) else []
|
|
119
124
|
patterns = prefs.get("ignore_prompt_patterns")
|
|
@@ -135,24 +140,35 @@ def _load_local_rules() -> dict:
|
|
|
135
140
|
return data
|
|
136
141
|
|
|
137
142
|
|
|
138
|
-
def
|
|
143
|
+
def automated_text_rule(text: str) -> str:
|
|
144
|
+
"""The built-in or local rule that marks `text` as automation, or "" when none does."""
|
|
139
145
|
if not text:
|
|
140
|
-
return
|
|
146
|
+
return ""
|
|
141
147
|
s = str(text).strip()
|
|
142
148
|
if not s:
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
return ""
|
|
150
|
+
for p in _BUILTIN_AUTOMATED_PREFIXES:
|
|
151
|
+
if s.startswith(p):
|
|
152
|
+
return p
|
|
153
|
+
for m in _BUILTIN_AUTOMATED_SUBSTRINGS:
|
|
154
|
+
if m in s:
|
|
155
|
+
return m
|
|
146
156
|
low = s.casefold()
|
|
147
157
|
rules = _load_local_rules()
|
|
148
158
|
# Local rules match case-insensitively (casefold) so user-supplied patterns need not mirror exact casing.
|
|
149
159
|
prefixes = _LOCAL_RULES_CACHE.get("folded_prefixes") or tuple(p.casefold() for p in (rules.get("automated_prefixes") or []))
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
for p in prefixes:
|
|
161
|
+
if low.startswith(p):
|
|
162
|
+
return p
|
|
152
163
|
substrings = _LOCAL_RULES_CACHE.get("folded_substrings") or tuple(m.casefold() for m in (rules.get("automated_substrings") or []))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
for m in substrings:
|
|
165
|
+
if m in low:
|
|
166
|
+
return "has:" + m
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def is_automated_or_subagent_text(text: str) -> bool:
|
|
171
|
+
return bool(automated_text_rule(text))
|
|
156
172
|
|
|
157
173
|
|
|
158
174
|
def is_users_own_prompt(text: str) -> bool:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Subprocess runner and Terminal helpers: `run`, AppleScript quoting, the Terminal focus script, resume commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from mixhub.proc.identity import PS_ENV
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(args, timeout=5):
|
|
14
|
+
env = {**os.environ, **PS_ENV} if (args and os.path.basename(str(args[0])) == "ps") else None
|
|
15
|
+
result = subprocess.run(args, capture_output=True, text=True, timeout=timeout, env=env)
|
|
16
|
+
if result.returncode:
|
|
17
|
+
raise RuntimeError(result.stderr.strip()[:400] or "Command failed: " + args[0])
|
|
18
|
+
return result.stdout
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_DEFAULT_RUN = run
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def apple_string(text):
|
|
25
|
+
return '"' + text.replace("\\", "\\\\").replace('"', '\\"').replace("\r", "\\r").replace("\n", "\\n") + '"'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def focus_script(tty):
|
|
29
|
+
return '''tell application "Terminal"
|
|
30
|
+
repeat with w in windows
|
|
31
|
+
repeat with t in tabs of w
|
|
32
|
+
if tty of t is %s then
|
|
33
|
+
set selected tab of w to t
|
|
34
|
+
set index of w to 1
|
|
35
|
+
activate
|
|
36
|
+
return "focused"
|
|
37
|
+
end if
|
|
38
|
+
end repeat
|
|
39
|
+
end repeat
|
|
40
|
+
error "No Terminal tab matches this tty"
|
|
41
|
+
end tell''' % apple_string("/dev/" + tty.removeprefix("/dev/"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _claude_project_dir_name(cwd: str) -> str:
|
|
45
|
+
return re.sub(r"[^A-Za-z0-9]", "-", cwd)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resume_cwd(session: dict) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Folder to resume `session` from. Claude Code looks a transcript up by the folder it runs in, so a session whose
|
|
51
|
+
cwd moved after it started (`cd` inside it) resumes from a folder its transcript belongs to: the session's own
|
|
52
|
+
cwd when that matches, else the first cwd its transcript records that does. Other tools use the session's cwd.
|
|
53
|
+
"""
|
|
54
|
+
home = os.path.expanduser("~")
|
|
55
|
+
cwd = session.get("cwd") or home
|
|
56
|
+
path = str(session.get("path") or "")
|
|
57
|
+
if session.get("tool") != "claude" or not path:
|
|
58
|
+
return cwd
|
|
59
|
+
want = os.path.basename(os.path.dirname(path))
|
|
60
|
+
if _claude_project_dir_name(cwd) == want:
|
|
61
|
+
return cwd
|
|
62
|
+
try:
|
|
63
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
64
|
+
for _, line in zip(range(2000), f):
|
|
65
|
+
if '"cwd"' not in line:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
cand = json.loads(line).get("cwd")
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
if isinstance(cand, str) and _claude_project_dir_name(cand) == want and os.path.isdir(cand):
|
|
72
|
+
return cand
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
return home if _claude_project_dir_name(home) == want else cwd
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resume_command(session):
|
|
79
|
+
tool, sid = session["tool"], session["id"]
|
|
80
|
+
if session.get("synthetic"):
|
|
81
|
+
suffix = tool
|
|
82
|
+
else:
|
|
83
|
+
suffix = {"claude": "claude --resume ", "agy": "agy --conversation ", "codex": "codex resume ",
|
|
84
|
+
"opencode": "opencode -s "}[tool] + shlex.quote(sid)
|
|
85
|
+
return "cd " + shlex.quote(resume_cwd(session)) + " && " + suffix
|
|
@@ -17,7 +17,7 @@ from mixhub.hub.constants import (
|
|
|
17
17
|
)
|
|
18
18
|
from mixhub.hub.opencode import opencode_entries
|
|
19
19
|
from mixhub.hub.schema import claude_is_headless_marker, claude_is_synthetic_user, PROVIDER_SCHEMA_ADAPTERS
|
|
20
|
-
from mixhub.hub.session_filters import
|
|
20
|
+
from mixhub.hub.session_filters import codex_automation_reason, is_automated_or_subagent_text
|
|
21
21
|
from mixhub.hub.text import clean_prompt, snippet
|
|
22
22
|
from mixhub.hub.titles import (
|
|
23
23
|
_is_generic_placeholder_title,
|
|
@@ -266,8 +266,10 @@ class TranscriptMixin:
|
|
|
266
266
|
if row.get("type") == "session_meta":
|
|
267
267
|
meta = row.get("payload") or {}
|
|
268
268
|
result.update({k: meta[k] for k in ("id", "cwd") if meta.get(k)})
|
|
269
|
-
|
|
269
|
+
auto_by = codex_automation_reason(meta.get("source"), originator=meta.get("originator"))
|
|
270
|
+
if auto_by:
|
|
270
271
|
result["automated"] = True
|
|
272
|
+
result["automatedBy"] = auto_by
|
|
271
273
|
if row.get("lastPrompt"):
|
|
272
274
|
result["lastPrompt"] = snippet(clean_prompt(row["lastPrompt"], tool), 220)
|
|
273
275
|
if has_sdk_cli and not has_human_turn:
|
|
@@ -302,8 +304,11 @@ class TranscriptMixin:
|
|
|
302
304
|
result["messageCount"] += 1
|
|
303
305
|
if entry["role"] == "user" and not entry.get("synthetic"):
|
|
304
306
|
prompt = snippet(entry["text"])
|
|
305
|
-
if tool == "codex" and not result["firstPrompt"]
|
|
306
|
-
|
|
307
|
+
if tool == "codex" and not result["firstPrompt"]:
|
|
308
|
+
auto_by = codex_automation_reason(None, "", entry["text"])
|
|
309
|
+
if auto_by:
|
|
310
|
+
result["automated"] = True
|
|
311
|
+
result.setdefault("automatedBy", auto_by)
|
|
307
312
|
result["firstPrompt"] = result["firstPrompt"] or prompt
|
|
308
313
|
if not substantive_prompt and not _is_generic_placeholder_title(prompt) and len(prompt.strip()) >= 4:
|
|
309
314
|
substantive_prompt = prompt
|