hive-ide 1.0.0__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.
Files changed (54) hide show
  1. hive_ide-1.0.0/CHANGELOG.md +16 -0
  2. hive_ide-1.0.0/CONTRIBUTING.md +14 -0
  3. hive_ide-1.0.0/LICENSE +21 -0
  4. hive_ide-1.0.0/MANIFEST.in +2 -0
  5. hive_ide-1.0.0/PKG-INFO +138 -0
  6. hive_ide-1.0.0/README.md +107 -0
  7. hive_ide-1.0.0/hive_ide/__init__.py +13 -0
  8. hive_ide-1.0.0/hive_ide/__main__.py +3 -0
  9. hive_ide-1.0.0/hive_ide/activity.py +65 -0
  10. hive_ide-1.0.0/hive_ide/adapters.py +57 -0
  11. hive_ide-1.0.0/hive_ide/agentmodal.py +207 -0
  12. hive_ide-1.0.0/hive_ide/cli.py +692 -0
  13. hive_ide-1.0.0/hive_ide/config.py +296 -0
  14. hive_ide-1.0.0/hive_ide/drivers.py +162 -0
  15. hive_ide-1.0.0/hive_ide/environments.py +73 -0
  16. hive_ide-1.0.0/hive_ide/errormodal.py +43 -0
  17. hive_ide-1.0.0/hive_ide/errors.py +17 -0
  18. hive_ide-1.0.0/hive_ide/frame.py +817 -0
  19. hive_ide-1.0.0/hive_ide/git_status.py +117 -0
  20. hive_ide-1.0.0/hive_ide/hook.py +149 -0
  21. hive_ide-1.0.0/hive_ide/hooks.py +232 -0
  22. hive_ide-1.0.0/hive_ide/info.py +77 -0
  23. hive_ide-1.0.0/hive_ide/layout.py +132 -0
  24. hive_ide-1.0.0/hive_ide/nav.py +140 -0
  25. hive_ide-1.0.0/hive_ide/newmodal.py +393 -0
  26. hive_ide-1.0.0/hive_ide/paths.py +42 -0
  27. hive_ide-1.0.0/hive_ide/popup.py +46 -0
  28. hive_ide-1.0.0/hive_ide/relayout.py +306 -0
  29. hive_ide-1.0.0/hive_ide/seen.py +83 -0
  30. hive_ide-1.0.0/hive_ide/sidebar.py +1090 -0
  31. hive_ide-1.0.0/hive_ide/sidebar_grid.py +118 -0
  32. hive_ide-1.0.0/hive_ide/sidebar_plugins.py +315 -0
  33. hive_ide-1.0.0/hive_ide/skill_definition/SKILL.md +33 -0
  34. hive_ide-1.0.0/hive_ide/source.py +97 -0
  35. hive_ide-1.0.0/hive_ide/state_compat.py +116 -0
  36. hive_ide-1.0.0/hive_ide/store.py +231 -0
  37. hive_ide-1.0.0/hive_ide.egg-info/PKG-INFO +138 -0
  38. hive_ide-1.0.0/hive_ide.egg-info/SOURCES.txt +52 -0
  39. hive_ide-1.0.0/hive_ide.egg-info/dependency_links.txt +1 -0
  40. hive_ide-1.0.0/hive_ide.egg-info/entry_points.txt +2 -0
  41. hive_ide-1.0.0/hive_ide.egg-info/requires.txt +7 -0
  42. hive_ide-1.0.0/hive_ide.egg-info/top_level.txt +1 -0
  43. hive_ide-1.0.0/pyproject.toml +53 -0
  44. hive_ide-1.0.0/setup.cfg +4 -0
  45. hive_ide-1.0.0/tests/test_ide_agentmodal.py +93 -0
  46. hive_ide-1.0.0/tests/test_ide_layout.py +175 -0
  47. hive_ide-1.0.0/tests/test_ide_modal_context.py +203 -0
  48. hive_ide-1.0.0/tests/test_ide_nav.py +48 -0
  49. hive_ide-1.0.0/tests/test_ide_newmodal.py +104 -0
  50. hive_ide-1.0.0/tests/test_ide_relayout.py +217 -0
  51. hive_ide-1.0.0/tests/test_ide_sidebar.py +482 -0
  52. hive_ide-1.0.0/tests/test_protocol.py +903 -0
  53. hive_ide-1.0.0/tests/test_public_vocabulary.py +77 -0
  54. hive_ide-1.0.0/tests/test_tmux_integration.py +244 -0
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to `hive-ide` will be documented in this file.
4
+
5
+ ## Unreleased
6
+
7
+ ## [1.0.0] - 2026-07-28
8
+
9
+ ### Added
10
+
11
+ - Standalone, directory-scoped session storage keyed by immutable session IDs.
12
+ - A tmux frame with responsive sidebar, agent pane, plan pane, and configurable keys.
13
+ - Claude Code, Codex, Antigravity, and terminal drivers behind plugin entry points.
14
+ - Configurable sidebar state and icon-slot providers.
15
+ - Stable and editable development environments with per-session source switching.
16
+ - Agent status and compaction lifecycle hooks.
@@ -0,0 +1,14 @@
1
+ # Contributing
2
+
3
+ `hive-ide` requires Python 3.10+, `tmux`, and a Unix-like system.
4
+
5
+ ```sh
6
+ python3 -m venv .venv-dev
7
+ .venv-dev/bin/python -m pip install -e ".[test,release]"
8
+ .venv-dev/bin/python -m pytest
9
+ .venv-dev/bin/python -m build
10
+ .venv-dev/bin/python -m twine check dist/*
11
+ ```
12
+
13
+ Keep the standalone package host-neutral. Host-specific workspace, plan, and command
14
+ behavior belongs behind adapters or plugin entry points.
hive_ide-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dynamite Ventures
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include CHANGELOG.md
2
+ include CONTRIBUTING.md
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: hive-ide
3
+ Version: 1.0.0
4
+ Summary: An agent-aware tmux workspace for coding agents
5
+ Author: Meli Hive
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/melihive/hive-ide
8
+ Project-URL: Changelog, https://github.com/melihive/hive-ide/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/melihive/hive-ide/issues
10
+ Project-URL: Source, https://github.com/melihive/hive-ide
11
+ Keywords: ai-agents,claude-code,codex,terminal,tmux
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8; extra == "test"
27
+ Provides-Extra: release
28
+ Requires-Dist: build>=1.2; extra == "release"
29
+ Requires-Dist: twine>=6; extra == "release"
30
+ Dynamic: license-file
31
+
32
+ # hive-ide
33
+
34
+ An agent-aware tmux workspace for running coding agents across multiple sessions.
35
+
36
+ > **Status: stable.** Version 1.x follows semantic versioning for its public interfaces.
37
+
38
+ ## Requirements
39
+
40
+ Unix (Linux/macOS), `tmux`, and Python 3.10+.
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ python3 -m pip install hive-ide
46
+ hive-ide create --name=MAIN --driver=term
47
+ hive-ide open
48
+ ```
49
+
50
+ Bundled drivers are `claude`, `codex`, `antigravity`, and `term`. State is user-local and
51
+ directory-scoped. Agent drivers require their corresponding local command; `term` opens a
52
+ regular shell.
53
+
54
+ An existing Git worktree is just a session working directory. The IDE attaches to it but
55
+ never creates, merges, or removes it:
56
+
57
+ ```sh
58
+ hive-ide create --name=FEATURE --driver=codex --working-dir=../project-feature
59
+ hive-ide working-dir-set --session-id=<ID> --working-dir=../project-feature
60
+ ```
61
+
62
+ The sidebar marks linked worktrees as clean, modified, missing, or unknown.
63
+
64
+ Reopen the current session's plan or resume its recorded agent conversation:
65
+
66
+ ```sh
67
+ hive-ide current-plan
68
+ hive-ide current-chat
69
+ ```
70
+
71
+ Both commands use `HIVE_IDE_SESSION_ID` inside the frame, or accept
72
+ `--session-id=<ID>`. They target the session pane when its frame is running and
73
+ otherwise open in the current terminal.
74
+
75
+ Local settings live in `~/.config/hive-ide/config.json` (or `HIVE_IDE_CONFIG`).
76
+ The plan editor accepts any command string or argv list. Resolution is configured
77
+ editor, `HIVE_IDE_EDITOR`, `micro` when installed, then `less`:
78
+
79
+ ```json
80
+ {
81
+ "editor": "nvim --clean",
82
+ "keys": {
83
+ "prefix": "C-a",
84
+ "bindings": {"next": "n", "previous": "p", "error": null}
85
+ },
86
+ "sidebar": {
87
+ "state": "activity",
88
+ "slots": ["plan", "checkout", "ci"],
89
+ "providers": {
90
+ "ci": {
91
+ "region": "slot",
92
+ "source": "session",
93
+ "path": ["host", "ci", "state"],
94
+ "icons": {"passing": "✓", "failing": "❌"}
95
+ }
96
+ },
97
+ "icons": {
98
+ "drivers": {"codex": "C"},
99
+ "providers": {"checkout": {"live": "W"}}
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ Sidebar symbols may occupy one or two terminal cells. `state` selects one mutually
106
+ exclusive provider; `slots` is an ordered list and may contain any number of columns.
107
+ Additional field providers can be declared in config. Python entry points in
108
+ `hive_ide.sidebar_providers` may contribute the same normalized field definitions; pane
109
+ processes read only the resulting JSON snapshot and never import plugin code.
110
+
111
+ Create managed stable and editable development environments, then switch one session
112
+ without affecting the others:
113
+
114
+ ```sh
115
+ hive-ide environment-setup --dev-checkout=.
116
+ hive-ide source-set --session-id=<ID> --source=dev
117
+ hive-ide source-set --session-id=<ID> --source=stable
118
+ ```
119
+
120
+ Preview machine-global Claude and Codex status hooks before applying them:
121
+
122
+ ```sh
123
+ hive-ide hook-setup
124
+ hive-ide hook-setup --apply
125
+ ```
126
+
127
+ ## Development
128
+
129
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
130
+
131
+ ## License
132
+
133
+ MIT. See [LICENSE](LICENSE).
134
+
135
+ ## About
136
+
137
+ `hive-ide` is part of [Meli Hive](https://melihive.com). It is being built as a standalone
138
+ MIT-licensed project with no dependency on the rest of the platform.
@@ -0,0 +1,107 @@
1
+ # hive-ide
2
+
3
+ An agent-aware tmux workspace for running coding agents across multiple sessions.
4
+
5
+ > **Status: stable.** Version 1.x follows semantic versioning for its public interfaces.
6
+
7
+ ## Requirements
8
+
9
+ Unix (Linux/macOS), `tmux`, and Python 3.10+.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ python3 -m pip install hive-ide
15
+ hive-ide create --name=MAIN --driver=term
16
+ hive-ide open
17
+ ```
18
+
19
+ Bundled drivers are `claude`, `codex`, `antigravity`, and `term`. State is user-local and
20
+ directory-scoped. Agent drivers require their corresponding local command; `term` opens a
21
+ regular shell.
22
+
23
+ An existing Git worktree is just a session working directory. The IDE attaches to it but
24
+ never creates, merges, or removes it:
25
+
26
+ ```sh
27
+ hive-ide create --name=FEATURE --driver=codex --working-dir=../project-feature
28
+ hive-ide working-dir-set --session-id=<ID> --working-dir=../project-feature
29
+ ```
30
+
31
+ The sidebar marks linked worktrees as clean, modified, missing, or unknown.
32
+
33
+ Reopen the current session's plan or resume its recorded agent conversation:
34
+
35
+ ```sh
36
+ hive-ide current-plan
37
+ hive-ide current-chat
38
+ ```
39
+
40
+ Both commands use `HIVE_IDE_SESSION_ID` inside the frame, or accept
41
+ `--session-id=<ID>`. They target the session pane when its frame is running and
42
+ otherwise open in the current terminal.
43
+
44
+ Local settings live in `~/.config/hive-ide/config.json` (or `HIVE_IDE_CONFIG`).
45
+ The plan editor accepts any command string or argv list. Resolution is configured
46
+ editor, `HIVE_IDE_EDITOR`, `micro` when installed, then `less`:
47
+
48
+ ```json
49
+ {
50
+ "editor": "nvim --clean",
51
+ "keys": {
52
+ "prefix": "C-a",
53
+ "bindings": {"next": "n", "previous": "p", "error": null}
54
+ },
55
+ "sidebar": {
56
+ "state": "activity",
57
+ "slots": ["plan", "checkout", "ci"],
58
+ "providers": {
59
+ "ci": {
60
+ "region": "slot",
61
+ "source": "session",
62
+ "path": ["host", "ci", "state"],
63
+ "icons": {"passing": "✓", "failing": "❌"}
64
+ }
65
+ },
66
+ "icons": {
67
+ "drivers": {"codex": "C"},
68
+ "providers": {"checkout": {"live": "W"}}
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Sidebar symbols may occupy one or two terminal cells. `state` selects one mutually
75
+ exclusive provider; `slots` is an ordered list and may contain any number of columns.
76
+ Additional field providers can be declared in config. Python entry points in
77
+ `hive_ide.sidebar_providers` may contribute the same normalized field definitions; pane
78
+ processes read only the resulting JSON snapshot and never import plugin code.
79
+
80
+ Create managed stable and editable development environments, then switch one session
81
+ without affecting the others:
82
+
83
+ ```sh
84
+ hive-ide environment-setup --dev-checkout=.
85
+ hive-ide source-set --session-id=<ID> --source=dev
86
+ hive-ide source-set --session-id=<ID> --source=stable
87
+ ```
88
+
89
+ Preview machine-global Claude and Codex status hooks before applying them:
90
+
91
+ ```sh
92
+ hive-ide hook-setup
93
+ hive-ide hook-setup --apply
94
+ ```
95
+
96
+ ## Development
97
+
98
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
99
+
100
+ ## License
101
+
102
+ MIT. See [LICENSE](LICENSE).
103
+
104
+ ## About
105
+
106
+ `hive-ide` is part of [Meli Hive](https://melihive.com). It is being built as a standalone
107
+ MIT-licensed project with no dependency on the rest of the platform.
@@ -0,0 +1,13 @@
1
+ """Public package surface for hive-ide."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ PROTOCOL_VERSION = 1
6
+ SCHEMA_VERSION = 1
7
+
8
+ try:
9
+ __version__ = version("hive-ide")
10
+ except PackageNotFoundError:
11
+ __version__ = "0.1.0.dev0"
12
+
13
+ __all__ = ["PROTOCOL_VERSION", "SCHEMA_VERSION", "__version__"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,65 @@
1
+ """Fail-open activity markers for processes running inside a session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from .store import StateStore, utc_now
8
+
9
+
10
+ class IdeActivity:
11
+ KIND_TASK = "task"
12
+ STATE_RUNNING = "running"
13
+ STATE_BLOCKED = "blocked"
14
+
15
+ @staticmethod
16
+ def _target() -> tuple[StateStore, str] | None:
17
+ state_home = os.environ.get("HIVE_IDE_STATE_HOME")
18
+ workspace = os.environ.get("HIVE_IDE_WORKSPACE_KEY")
19
+ session_id = os.environ.get("HIVE_IDE_SESSION_ID")
20
+ if not state_home or not workspace or not session_id:
21
+ return None
22
+ return StateStore(state_home, workspace), session_id
23
+
24
+ @staticmethod
25
+ def mark(kind: str, *, label: str = "", state: str = "running") -> bool:
26
+ try:
27
+ target = IdeActivity._target()
28
+ if target is None:
29
+ return False
30
+ store, session_id = target
31
+ with store.mutation_lock():
32
+ if store.find_session(session_id) is None:
33
+ return False
34
+ store.write(
35
+ "activity",
36
+ session_id,
37
+ {
38
+ "schema_version": 1,
39
+ "session_id": session_id,
40
+ "workspace_key": store.workspace_key,
41
+ "kind": kind,
42
+ "state": state,
43
+ "label": label,
44
+ "observed_at": utc_now(),
45
+ },
46
+ )
47
+ return True
48
+ except Exception:
49
+ return False
50
+
51
+ @staticmethod
52
+ def blocked(kind: str, *, label: str = "") -> bool:
53
+ return IdeActivity.mark(kind, label=label, state=IdeActivity.STATE_BLOCKED)
54
+
55
+ @staticmethod
56
+ def clear() -> bool:
57
+ try:
58
+ target = IdeActivity._target()
59
+ if target is None:
60
+ return False
61
+ store, session_id = target
62
+ with store.mutation_lock():
63
+ return store.delete("activity", session_id)
64
+ except Exception:
65
+ return False
@@ -0,0 +1,57 @@
1
+ """Optional host integration contracts.
2
+
3
+ The standalone core uses these neutral defaults. A host may provide richer behavior
4
+ to foreground commands; spawned panes receive only the resulting JSON state.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Protocol
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Workspace:
16
+ key: str
17
+ working_dir: str
18
+ metadata: dict[str, object]
19
+
20
+
21
+ class WorkspaceAdapter(Protocol):
22
+ def resolve(self, directory: Path) -> Workspace: ...
23
+
24
+
25
+ class PlanAdapter(Protocol):
26
+ def resolve(self, reference: str | None, workspace: Workspace) -> dict[str, object]: ...
27
+
28
+ def active_task(self, plan: dict[str, object]) -> str | None: ...
29
+
30
+
31
+ class CommandSurface(Protocol):
32
+ def before_create(self, workspace: Workspace, name: str) -> None: ...
33
+
34
+ def after_archive(self, session: dict[str, object]) -> None: ...
35
+
36
+
37
+ class DefaultWorkspaceAdapter:
38
+ def resolve(self, directory: Path) -> Workspace:
39
+ resolved = str(directory.expanduser().resolve())
40
+ return Workspace(key=resolved, working_dir=resolved, metadata={})
41
+
42
+
43
+ class DefaultPlanAdapter:
44
+ def resolve(self, reference: str | None, workspace: Workspace) -> dict[str, object]:
45
+ return {"path": reference, "active_task": None}
46
+
47
+ def active_task(self, plan: dict[str, object]) -> str | None:
48
+ value = plan.get("active_task")
49
+ return value if isinstance(value, str) else None
50
+
51
+
52
+ class DefaultCommandSurface:
53
+ def before_create(self, workspace: Workspace, name: str) -> None:
54
+ return None
55
+
56
+ def after_archive(self, session: dict[str, object]) -> None:
57
+ return None
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """Guided 'change agent' modal — arrow-select, ESC-cancelable — in a tmux popup.
3
+
4
+ The visual twin of `ide_newmodal`'s driver step, but for an existing session: pick the
5
+ occupant the middle pane should run with ↑/↓ (or j/k, or 1-4), Enter to switch, and
6
+ ESC to cancel. The currently-running occupant is
7
+ marked and pre-selected, so Enter-on-it is a deliberate no-op rather than a surprise
8
+ switch.
9
+
10
+ It deliberately reuses `IdeNewModal`'s primitives — the SSH-timing-safe `_getkey`, the
11
+ colour constants, and the `TYPES` list — so the two modals look identical and the one
12
+ piece of subtle input logic (the arrow-vs-Escape peek) lives in exactly one place.
13
+
14
+ Stdlib only (like `ide_newmodal`); raw cbreak input, so it must not boot the foreground
15
+ runtime. The final action invokes the public package CLI.
16
+
17
+ usage: python3 -I ide_agentmodal.py <skill_dir> <repo> <window>
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import subprocess
22
+ import argparse
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ from .newmodal import IdeNewModal
27
+ from .state_compat import StateIO
28
+
29
+ try: # POSIX only (tmux is Unix); keep importable elsewhere
30
+ import termios
31
+ import tty
32
+ except ImportError:
33
+ termios = None
34
+ tty = None
35
+
36
+
37
+ class IdeAgentModal:
38
+ """One popup: arrow-select the session's occupant → switch. ESC cancels."""
39
+
40
+ DEFAULT_AGENT = "claude" # mirrors _IdeCore.DEFAULT_AGENT; the modal must not import it
41
+
42
+ # ---- switch (testable, no tty) ----
43
+
44
+ @staticmethod
45
+ def _switch(skill_dir: Path, session_id: str, kind: str) -> bool:
46
+ """Shell the switch through the launcher — the same path `pick_agent` uses, so the
47
+ pane respawn/resume logic stays in one place (`switch_agent`)."""
48
+ ok, _ = IdeNewModal._cli(
49
+ skill_dir,
50
+ [
51
+ "switch-driver",
52
+ f"--session-id={session_id}",
53
+ f"--driver={kind}",
54
+ *(
55
+ [f"--tmux-socket={IdeNewModal._tmux_socket}"]
56
+ if IdeNewModal._tmux_socket
57
+ else []
58
+ ),
59
+ ],
60
+ )
61
+ return ok
62
+
63
+ @staticmethod
64
+ def _context(skill_dir: Path, argv: list[str]) -> tuple[str, str, str] | None:
65
+ """`(repo, session_id, display_name)` for this popup, else None.
66
+
67
+ Resolution order is deliberate — IDENTITY first, display name only as a fallback:
68
+
69
+ 1. Public session-id and workspace-key options stamped by the frame at build time.
70
+ Immutable: `ide rename`, tmux `automatic-rename`, and a hand rename all leave
71
+ them untouched, so this keeps working when the window is called something else.
72
+ 2. `#{session_name}` / `#{window_name}` — for a frame built before the tags existed
73
+ (a server that has not been reopened since). Correct today, but goes stale on the
74
+ first rename, which is exactly why it is second.
75
+ """
76
+ M = IdeNewModal
77
+ sid = M._ask_tmux("#{@hive_ide_session_id}")
78
+ repo = M._ask_tmux("#{@hive_ide_workspace_key}")
79
+ if sid and repo:
80
+ hit = StateIO.find_by_id(skill_dir, repo, sid)
81
+ if hit:
82
+ return hit[0], hit[2]["id"], hit[1]
83
+ # A stamped id that resolves to nothing is a REAL error (deleted record, wrong
84
+ # checkout's registry) — not a reason to quietly fall back to a name that might
85
+ # accidentally match a different session.
86
+ return None
87
+ repo = M._resolve(argv[2] if len(argv) > 2 else None, "#{session_name}")
88
+ window = M._resolve(argv[3] if len(argv) > 3 else None, "#{window_name}")
89
+ if not repo or not window:
90
+ return None
91
+ hit = StateIO.find_by_identity(skill_dir, repo, window)
92
+ return (hit[0], hit[2]["id"], hit[1]) if hit else None
93
+
94
+ @staticmethod
95
+ def _active(skill_dir: Path, repo: str, session_id: str) -> str | None:
96
+ """The occupant the session runs now — None if there is no such record (the modal
97
+ then has nothing to change and bails, rather than inventing a default)."""
98
+ found = StateIO.find_by_id(skill_dir, repo, session_id)
99
+ if found is None:
100
+ return None
101
+ rec = found[2]
102
+ return (
103
+ (rec.get("driver") or {}).get("id")
104
+ or (rec.get("agents") or {}).get("active")
105
+ or IdeAgentModal.DEFAULT_AGENT
106
+ )
107
+
108
+ # ---- interactive loop (tty) ----
109
+
110
+ @staticmethod
111
+ def _draw(window: str, repo: str, active: str, sel: int) -> None:
112
+ M = IdeNewModal
113
+ o = [M.CLR, f" {M.BOLD}Change agent{M.RST}\n"]
114
+ # Subtitle: which session, and what it runs now — the context a bare list lacks.
115
+ o.append(f" {M.DIM}{window} ({repo}) — running {active}{M.RST}{M.EL}\n\n")
116
+ for i, (kind, label, note) in enumerate(M.TYPES):
117
+ picked = i == sel
118
+ here = kind == active
119
+ arrow = "▸" if picked else " "
120
+ # The active occupant carries a trailing "· current" so it reads even when it is
121
+ # NOT the highlighted row — the highlight says "will switch here", the tag says
122
+ # "you are here", and the two must not be confused.
123
+ tag = f" {M.NAME}· current{M.RST}" if here else ""
124
+ if picked:
125
+ o.append(f" {M.SEL} {arrow} {label} {note} {M.RST}{tag}{M.EL}\n")
126
+ else:
127
+ o.append(f" {arrow} {label} {M.DIM}{note}{M.RST}{tag}{M.EL}\n")
128
+ o.append(f"\n {M.DIM}↑/↓ or j/k · 1-4 or Enter → switch · Esc → cancel{M.RST}{M.EL}")
129
+ sys.stdout.write("".join(o))
130
+ sys.stdout.flush()
131
+
132
+ @staticmethod
133
+ def main(argv: list[str]) -> int:
134
+ raw = argv[1:]
135
+ if "--state-home" in raw:
136
+ parser = argparse.ArgumentParser(prog="python -m hive_ide.agentmodal")
137
+ parser.add_argument("--state-home", required=True)
138
+ parser.add_argument("--workspace-key", required=True)
139
+ parser.add_argument("--session-id", required=True)
140
+ parser.add_argument("--tmux-socket")
141
+ parsed = parser.parse_args(raw)
142
+ skill_dir = Path(parsed.state_home)
143
+ IdeNewModal._tmux_socket = parsed.tmux_socket or ""
144
+ found = StateIO.find_by_id(skill_dir, parsed.workspace_key, parsed.session_id)
145
+ ctx = (found[0], found[2]["id"], found[1]) if found else None
146
+ else:
147
+ if len(argv) < 2:
148
+ sys.stderr.write("usage: ide_agentmodal.py <state_home> [workspace] [window]\n")
149
+ return 2
150
+ skill_dir = Path(argv[1])
151
+ ctx = IdeAgentModal._context(skill_dir, argv)
152
+ # `repo`/`window` are OPTIONAL and self-resolving. They used to be passed as tmux
153
+ # formats from the key binding — but `display-popup` does not expand formats in its
154
+ # command, so the modal received the literal `#{session_name}` / `#{window_name}`,
155
+ # found no record, and closed instantly. That was the "flashes and closes" bug.
156
+ if ctx is None:
157
+ return IdeNewModal._bail(
158
+ "Could not determine which ide session this window is.",
159
+ "no @hive_ide_session_id on the window and no resolvable name — "
160
+ "run `hive-ide open` to rebuild the frame, or `hive-ide rebuild`.")
161
+ repo, session_id, window = ctx
162
+ IdeNewModal._workspace_key = repo
163
+ if not (termios and tty and sys.stdin.isatty()):
164
+ return 0 # interactive-only; no tty → nothing to do
165
+ active = IdeAgentModal._active(skill_dir, repo, session_id)
166
+ if active is None:
167
+ return IdeNewModal._bail(
168
+ f"No ide session record for id '{session_id}'.",
169
+ f"looked in the protocol session registry for workspace {repo}")
170
+ types = IdeNewModal.TYPES
171
+ # Pre-select the active occupant, so opening the modal and pressing Enter keeps
172
+ # things as they are — the safe default for a picker over an existing session.
173
+ sel = next((i for i, (k, *_ ) in enumerate(types) if k == active), 0)
174
+ fd = sys.stdin.fileno()
175
+ saved = termios.tcgetattr(fd)
176
+ tty.setcbreak(fd)
177
+ sys.stdout.write("\x1b[?25l") # hide the real cursor
178
+ sys.stdout.flush()
179
+ result = None
180
+ try:
181
+ while True:
182
+ IdeAgentModal._draw(window, repo, active, sel)
183
+ k = IdeNewModal._getkey(fd)
184
+ if k == "esc":
185
+ return 0
186
+ if k in ("up", "k"):
187
+ sel = (sel - 1) % len(types)
188
+ elif k in ("down", "j"):
189
+ sel = (sel + 1) % len(types)
190
+ elif k.isdigit() and 1 <= int(k) <= len(types):
191
+ result = types[int(k) - 1][0]
192
+ break
193
+ elif k == "enter":
194
+ result = types[sel][0]
195
+ break
196
+ finally:
197
+ sys.stdout.write("\x1b[?25h") # restore the cursor
198
+ termios.tcsetattr(fd, termios.TCSADRAIN, saved)
199
+ sys.stdout.write(IdeNewModal.CLR)
200
+ sys.stdout.flush()
201
+ if result is None or result == active:
202
+ return 0 # cancelled, or picked what's already running
203
+ return 0 if IdeAgentModal._switch(skill_dir, session_id, result) else 1
204
+
205
+
206
+ if __name__ == "__main__":
207
+ sys.exit(IdeAgentModal.main(sys.argv))