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.
- hive_ide-1.0.0/CHANGELOG.md +16 -0
- hive_ide-1.0.0/CONTRIBUTING.md +14 -0
- hive_ide-1.0.0/LICENSE +21 -0
- hive_ide-1.0.0/MANIFEST.in +2 -0
- hive_ide-1.0.0/PKG-INFO +138 -0
- hive_ide-1.0.0/README.md +107 -0
- hive_ide-1.0.0/hive_ide/__init__.py +13 -0
- hive_ide-1.0.0/hive_ide/__main__.py +3 -0
- hive_ide-1.0.0/hive_ide/activity.py +65 -0
- hive_ide-1.0.0/hive_ide/adapters.py +57 -0
- hive_ide-1.0.0/hive_ide/agentmodal.py +207 -0
- hive_ide-1.0.0/hive_ide/cli.py +692 -0
- hive_ide-1.0.0/hive_ide/config.py +296 -0
- hive_ide-1.0.0/hive_ide/drivers.py +162 -0
- hive_ide-1.0.0/hive_ide/environments.py +73 -0
- hive_ide-1.0.0/hive_ide/errormodal.py +43 -0
- hive_ide-1.0.0/hive_ide/errors.py +17 -0
- hive_ide-1.0.0/hive_ide/frame.py +817 -0
- hive_ide-1.0.0/hive_ide/git_status.py +117 -0
- hive_ide-1.0.0/hive_ide/hook.py +149 -0
- hive_ide-1.0.0/hive_ide/hooks.py +232 -0
- hive_ide-1.0.0/hive_ide/info.py +77 -0
- hive_ide-1.0.0/hive_ide/layout.py +132 -0
- hive_ide-1.0.0/hive_ide/nav.py +140 -0
- hive_ide-1.0.0/hive_ide/newmodal.py +393 -0
- hive_ide-1.0.0/hive_ide/paths.py +42 -0
- hive_ide-1.0.0/hive_ide/popup.py +46 -0
- hive_ide-1.0.0/hive_ide/relayout.py +306 -0
- hive_ide-1.0.0/hive_ide/seen.py +83 -0
- hive_ide-1.0.0/hive_ide/sidebar.py +1090 -0
- hive_ide-1.0.0/hive_ide/sidebar_grid.py +118 -0
- hive_ide-1.0.0/hive_ide/sidebar_plugins.py +315 -0
- hive_ide-1.0.0/hive_ide/skill_definition/SKILL.md +33 -0
- hive_ide-1.0.0/hive_ide/source.py +97 -0
- hive_ide-1.0.0/hive_ide/state_compat.py +116 -0
- hive_ide-1.0.0/hive_ide/store.py +231 -0
- hive_ide-1.0.0/hive_ide.egg-info/PKG-INFO +138 -0
- hive_ide-1.0.0/hive_ide.egg-info/SOURCES.txt +52 -0
- hive_ide-1.0.0/hive_ide.egg-info/dependency_links.txt +1 -0
- hive_ide-1.0.0/hive_ide.egg-info/entry_points.txt +2 -0
- hive_ide-1.0.0/hive_ide.egg-info/requires.txt +7 -0
- hive_ide-1.0.0/hive_ide.egg-info/top_level.txt +1 -0
- hive_ide-1.0.0/pyproject.toml +53 -0
- hive_ide-1.0.0/setup.cfg +4 -0
- hive_ide-1.0.0/tests/test_ide_agentmodal.py +93 -0
- hive_ide-1.0.0/tests/test_ide_layout.py +175 -0
- hive_ide-1.0.0/tests/test_ide_modal_context.py +203 -0
- hive_ide-1.0.0/tests/test_ide_nav.py +48 -0
- hive_ide-1.0.0/tests/test_ide_newmodal.py +104 -0
- hive_ide-1.0.0/tests/test_ide_relayout.py +217 -0
- hive_ide-1.0.0/tests/test_ide_sidebar.py +482 -0
- hive_ide-1.0.0/tests/test_protocol.py +903 -0
- hive_ide-1.0.0/tests/test_public_vocabulary.py +77 -0
- 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.
|
hive_ide-1.0.0/PKG-INFO
ADDED
|
@@ -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.
|
hive_ide-1.0.0/README.md
ADDED
|
@@ -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,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))
|