@scemoon/cdh 1.0.0 → 1.0.3

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 (116) hide show
  1. package/cdh/__pycache__/__main__.cpython-314.pyc +0 -0
  2. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  3. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  4. package/cdh/cli.py +473 -87
  5. package/cdh/scaffold.py +386 -121
  6. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  7. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  8. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  9. package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
  10. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  11. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  12. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  13. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  14. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  15. package/onecode/agent/agents/types.py +123 -79
  16. package/onecode/agent/cdh_loader.py +26 -13
  17. package/onecode/agent/engine.py +426 -235
  18. package/onecode/agent/onecode_agent_acp.py +200 -73
  19. package/onecode/agent/session.py +8 -13
  20. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  21. package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
  22. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  23. package/onecode/agent/tools/agent_tools.py +4 -4
  24. package/onecode/agent/tools/todo_tools.py +188 -0
  25. package/onecode/cli.py +3 -3
  26. package/onecode/commands.py +439 -194
  27. package/onecode/config.py +3 -3
  28. package/onecode/config_screen.py +4 -2
  29. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  30. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  31. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  32. package/onecode/models/errors.py +11 -1
  33. package/onecode/models/messages.py +35 -16
  34. package/onecode/models/provider.py +243 -12
  35. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  36. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  37. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  38. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  39. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  40. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  41. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  42. package/onecode/models/providers/anthropic_provider.py +4 -2
  43. package/onecode/models/providers/deepseek_provider.py +2 -0
  44. package/onecode/models/providers/glm_provider.py +2 -1
  45. package/onecode/models/providers/minimax_provider.py +2 -1
  46. package/onecode/models/providers/minimaxi_provider.py +76 -25
  47. package/onecode/models/providers/ollama_provider.py +2 -1
  48. package/onecode/models/providers/openai_provider.py +4 -0
  49. package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
  50. package/onecode/tasks/__pycache__/__init__.cpython-314.pyc +0 -0
  51. package/onecode/tasks/__pycache__/manager.cpython-314.pyc +0 -0
  52. package/onecode/tasks/__pycache__/models.cpython-314.pyc +0 -0
  53. package/onecode/tasks/manager.py +1 -1
  54. package/package.json +1 -1
  55. package/pyproject.toml +10 -0
  56. package/run.js +24 -7
  57. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  58. package/tui/__pycache__/cli.cpython-314.pyc +0 -0
  59. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  60. package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
  61. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  62. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  63. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  64. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  65. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  66. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  67. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  68. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  69. package/tui/acp/agent.py +60 -11
  70. package/tui/acp/api.py +6 -0
  71. package/tui/acp/messages.py +11 -0
  72. package/tui/acp/prompt.py +1 -1
  73. package/tui/acp/protocol.py +31 -0
  74. package/tui/app.py +60 -8
  75. package/tui/db.py +23 -0
  76. package/tui/directory_watcher.py +35 -1
  77. package/tui/messages.py +4 -1
  78. package/tui/paths.py +3 -3
  79. package/tui/screens/__pycache__/agent_modal.cpython-314.pyc +0 -0
  80. package/tui/screens/__pycache__/component_picker.cpython-314.pyc +0 -0
  81. package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
  82. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  83. package/tui/screens/__pycache__/projects_app.cpython-314.pyc +0 -0
  84. package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
  85. package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
  86. package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
  87. package/tui/screens/component_picker.py +148 -0
  88. package/tui/screens/main.py +47 -95
  89. package/tui/screens/projects.tcss +10 -0
  90. package/tui/screens/projects_app.py +33 -5
  91. package/tui/screens/projects_screen.py +171 -9
  92. package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
  93. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  94. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  95. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  96. package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
  97. package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
  98. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  99. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  100. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  101. package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
  102. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  103. package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
  104. package/tui/widgets/__pycache__/subagent_screen.cpython-314.pyc +0 -0
  105. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  106. package/tui/widgets/ask_user.py +1 -1
  107. package/tui/widgets/conversation.py +99 -42
  108. package/tui/widgets/modified_files.py +155 -23
  109. package/tui/widgets/plan.py +22 -0
  110. package/tui/widgets/project_directory_tree.py +22 -10
  111. package/tui/widgets/project_grid_select.py +23 -1
  112. package/tui/widgets/question.py +1 -1
  113. package/tui/widgets/subagent.py +202 -36
  114. package/tui/widgets/subagent_screen.py +186 -0
  115. package/tui/widgets/tool_call.py +5 -8
  116. package/onecode/agent/tools/task_tools.py +0 -249
package/tui/app.py CHANGED
@@ -363,6 +363,7 @@ class A2TUIApp(App, inherit_bindings=False):
363
363
  session_id = getattr(conv.agent, "session_id", None) or conv._agent_session_id or ""
364
364
  self._exit_metrics = {
365
365
  "session_id": session_id,
366
+ "session_pk": conv._session_pk,
366
367
  "tool_call_total": conv._tool_call_total,
367
368
  "tool_call_success": conv._tool_call_success,
368
369
  "tool_call_failed": conv._tool_call_failed,
@@ -876,11 +877,16 @@ class A2TUIApp(App, inherit_bindings=False):
876
877
  )
877
878
  return
878
879
  if agent_identity:
879
- self.launch_agent(
880
- agent_identity,
881
- agent_session_id=agent_session_id,
882
- )
883
- return
880
+ try:
881
+ from tui.agents import read_agents
882
+ if agent_identity in await read_agents():
883
+ self.launch_agent(
884
+ agent_identity,
885
+ agent_session_id=agent_session_id,
886
+ )
887
+ return
888
+ except Exception:
889
+ pass
884
890
 
885
891
  if agent_identity := self._resolve_launch_agent():
886
892
  self.launch_agent(agent_identity, project_path=project_path)
@@ -999,9 +1005,27 @@ class A2TUIApp(App, inherit_bindings=False):
999
1005
  except Exception:
1000
1006
  pass
1001
1007
  sid = (self._exit_metrics.get("session_id", "") or "")[:36] if self._exit_metrics else ""
1008
+ session_pk = self._exit_metrics.get("session_pk") if self._exit_metrics else None
1009
+ # Format the session ID for display: prefer the short `cdh
1010
+ # session list` form (`session-<pk>` + agent_session_id prefix)
1011
+ # so the displayed value matches what users see in
1012
+ # `cdh session list` and what they pass to `cdh session load`.
1013
+ if session_pk is not None:
1014
+ display_id = f"session-{session_pk} ({sid[:8]})" if sid else f"session-{session_pk}"
1015
+ hint = f" Reload with: cdh session load {session_pk}\n"
1016
+ elif sid:
1017
+ display_id = sid
1018
+ hint = ""
1019
+ else:
1020
+ display_id = "-"
1021
+ hint = ""
1002
1022
  try:
1003
1023
  with open("/dev/tty", "w") as tty:
1004
- tty.write(f"\n CDH powering down. Goodbye!\n Session ID: {sid or '-'}\n")
1024
+ tty.write(
1025
+ f"\n CDH powering down. Goodbye!\n"
1026
+ f" Session ID: {display_id}\n"
1027
+ f"{hint}"
1028
+ )
1005
1029
  except Exception:
1006
1030
  pass
1007
1031
  import os
@@ -1079,10 +1103,23 @@ class A2TUIApp(App, inherit_bindings=False):
1079
1103
  return
1080
1104
  agent_identity = session["agent_identity"]
1081
1105
  agent_session_id = session.get("agent_session_id")
1106
+
1107
+ project_path = None
1108
+ try:
1109
+ meta = json.loads(session.get("meta_json", "{}"))
1110
+ if cwd := meta.get("cwd"):
1111
+ project_path = Path(cwd)
1112
+ except (json.JSONDecodeError, TypeError):
1113
+ pass
1114
+
1115
+ if project_path:
1116
+ self.project_dir = project_path
1117
+
1082
1118
  self.launch_agent(
1083
1119
  agent_identity,
1084
1120
  agent_session_id=agent_session_id,
1085
1121
  session_pk=session_pk,
1122
+ project_path=project_path,
1086
1123
  )
1087
1124
 
1088
1125
  @work
@@ -1110,6 +1147,17 @@ class A2TUIApp(App, inherit_bindings=False):
1110
1147
  import yaml
1111
1148
 
1112
1149
  result = await self.push_screen_wait("projects")
1150
+ if result is None:
1151
+ cfg = load_config()
1152
+ if cfg.current_project:
1153
+ projects_dir = CLOUD_DEV_HARNESS_DIR / "projects"
1154
+ pf = projects_dir / f"{cfg.current_project}.yaml"
1155
+ if not pf.exists():
1156
+ cfg.current_project = None
1157
+ cfg.current_project_path = None
1158
+ save_config(cfg)
1159
+ self.project_dir = None
1160
+ self.screen.post_message(messages.ProjectDirectoryUpdated(project_dir=None))
1113
1161
  if result is not None:
1114
1162
  if result == "__new__":
1115
1163
  default_path = str((self.project_dir or Path.cwd()).resolve())
@@ -1137,7 +1185,7 @@ class A2TUIApp(App, inherit_bindings=False):
1137
1185
  cfg.current_project_path = str(project_path)
1138
1186
  save_config(cfg)
1139
1187
  self.project_dir = project_path
1140
- self.screen.post_message(messages.ProjectDirectoryUpdated())
1188
+ self.screen.post_message(messages.ProjectDirectoryUpdated(project_dir=self.project_dir))
1141
1189
  self.notify(f"Created project '{name}' at {project_path}")
1142
1190
  return
1143
1191
 
@@ -1159,8 +1207,12 @@ class A2TUIApp(App, inherit_bindings=False):
1159
1207
 
1160
1208
  new_project_dir = Path(project_path) if project_path else Path.cwd()
1161
1209
  self.project_dir = new_project_dir
1162
- self.screen.post_message(messages.ProjectDirectoryUpdated())
1210
+ self.screen.post_message(messages.ProjectDirectoryUpdated(project_dir=self.project_dir))
1163
1211
  self.notify(f"Switched to project: {project_name}")
1212
+ from tui.agents import read_agents
1213
+ agents = await read_agents()
1214
+ if cfg.default_mode in agents:
1215
+ self.launch_agent(cfg.default_mode, project_path=new_project_dir)
1164
1216
 
1165
1217
  @on(messages.LaunchAgent)
1166
1218
  def on_launch_agent(self, message: messages.LaunchAgent) -> None:
package/tui/db.py CHANGED
@@ -172,6 +172,29 @@ class DB:
172
172
  except aiosqlite.Error:
173
173
  return False
174
174
 
175
+ async def session_get_by_agent_session_id(self, agent_session_id: str) -> Session | None:
176
+ """Get a session from its agent_session_id.
177
+
178
+ Args:
179
+ agent_session_id: The agent's session id (UUID).
180
+
181
+ Returns:
182
+ A Session if one is found, or `None`.
183
+ """
184
+ try:
185
+ async with self.open() as db:
186
+ db.row_factory = aiosqlite.Row
187
+ cursor = await db.execute(
188
+ "SELECT * from sessions WHERE agent_session_id = ?",
189
+ (agent_session_id,),
190
+ )
191
+ row = await cursor.fetchone()
192
+ except aiosqlite.Error:
193
+ return None
194
+ if row is None:
195
+ return None
196
+ return cast(Session, dict(row))
197
+
175
198
  async def session_get_recent(self, max_results: int = 100) -> list[Session] | None:
176
199
  """Get the most recent sessions."""
177
200
  try:
@@ -28,6 +28,32 @@ class DirectoryChanged(Message):
28
28
  return isinstance(message, DirectoryChanged)
29
29
 
30
30
 
31
+ _NOISY_DIR_NAMES = frozenset(
32
+ {
33
+ ".venv",
34
+ ".git",
35
+ ".pytest_cache",
36
+ "__pycache__",
37
+ ".mypy_cache",
38
+ "node_modules",
39
+ ".uv-cache",
40
+ "dist",
41
+ "build",
42
+ ".idea",
43
+ }
44
+ )
45
+
46
+
47
+ def _is_noisy_path(path: str) -> bool:
48
+ """Return True when *path* lives inside a directory we want to
49
+ ignore for change notifications (virtualenvs, caches, build output…).
50
+ """
51
+ if not path:
52
+ return False
53
+ parts = path.replace("\\", "/").split("/")
54
+ return any(part in _NOISY_DIR_NAMES for part in parts)
55
+
56
+
31
57
  class _PathEventDispatcher(FileSystemEventHandler):
32
58
  """Dispatches file system events to multiple DirectoryWatcher instances."""
33
59
 
@@ -53,7 +79,15 @@ class _PathEventDispatcher(FileSystemEventHandler):
53
79
  return bool(self._watchers)
54
80
 
55
81
  def on_any_event(self, event: FileSystemEvent) -> None:
56
- """Dispatch events to all registered watchers."""
82
+ """Dispatch events to all registered watchers.
83
+
84
+ Events for paths inside noisy directories (virtualenvs, caches,
85
+ build output, etc.) are dropped so they don't flood the UI.
86
+ """
87
+ src = getattr(event, "src_path", "") or ""
88
+ dest = getattr(event, "dest_path", "") or ""
89
+ if _is_noisy_path(src) or _is_noisy_path(dest):
90
+ return
57
91
  with self._lock:
58
92
  watchers = list(self._watchers)
59
93
 
package/tui/messages.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
-
2
+ from pathlib import Path
3
3
  from typing import Literal
4
4
 
5
5
  from textual.content import Content
@@ -72,9 +72,12 @@ class Flash(Message):
72
72
  duration: float | None = None
73
73
 
74
74
 
75
+ @dataclass
75
76
  class ProjectDirectoryUpdated(Message):
76
77
  """The project directory may may changed."""
77
78
 
79
+ project_dir: Path | None = None
80
+
78
81
 
79
82
  @dataclass
80
83
  class SessionNavigate(Message):
package/tui/paths.py CHANGED
@@ -2,7 +2,7 @@ from contextlib import suppress
2
2
  from pathlib import Path
3
3
  from typing import Final
4
4
 
5
- from xdg_base_dirs import xdg_config_home, xdg_data_home, xdg_state_home
5
+ from xdg_base_dirs import xdg_config_home, xdg_data_home
6
6
 
7
7
 
8
8
  APP_NAME: Final[str] = "tui"
@@ -39,7 +39,7 @@ def get_config() -> Path:
39
39
 
40
40
  def get_state() -> Path:
41
41
  """Return (possibly creating) the application state directory."""
42
- path = xdg_state_home() / APP_NAME
42
+ path = Path.home() / ".cdh" / "state" / APP_NAME
43
43
  with suppress(OSError):
44
44
  path.mkdir(0o700, exist_ok=True, parents=True)
45
45
  return path
@@ -65,7 +65,7 @@ def get_log() -> Path:
65
65
  Path to log directory.
66
66
 
67
67
  """
68
- path = Path.home() / ".onecode" / "logs"
68
+ path = Path.home() / ".cdh" / "logs"
69
69
  with suppress(OSError):
70
70
  path.mkdir(0o700, exist_ok=True, parents=True)
71
71
  return path
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from textual import on
6
+ from textual.app import ComposeResult
7
+ from textual.binding import Binding
8
+ from textual.containers import Horizontal, Vertical, VerticalScroll
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import Button, Checkbox, Footer, Static
11
+
12
+
13
+ class ComponentPickerScreen(ModalScreen[Optional[list[str]]]):
14
+ """Modal screen for selecting application components.
15
+
16
+ Returns the list of selected component ids, or None if cancelled.
17
+ If ``allow_empty`` is False (default), confirming with no selection
18
+ shows a notification and does not dismiss the screen.
19
+ """
20
+
21
+ BINDINGS = [
22
+ Binding("escape", "cancel", "Cancel", show=False),
23
+ ]
24
+
25
+ CSS = """
26
+ ComponentPickerScreen {
27
+ align: center middle;
28
+ background: rgba(0, 0, 0, 0.7);
29
+ }
30
+
31
+ #picker-dialog {
32
+ width: 78;
33
+ height: auto;
34
+ max-height: 90%;
35
+ background: #111;
36
+ border: solid #555;
37
+ padding: 0 1;
38
+ }
39
+
40
+ #picker-title {
41
+ height: 1;
42
+ background: #333;
43
+ color: #fff;
44
+ padding: 0 1;
45
+ text-style: bold;
46
+ }
47
+
48
+ #picker-subtitle {
49
+ height: 1;
50
+ color: #aaa;
51
+ padding: 0 1;
52
+ margin-bottom: 1;
53
+ }
54
+
55
+ #picker-list {
56
+ height: auto;
57
+ max-height: 22;
58
+ padding: 0 1;
59
+ }
60
+
61
+ .component-row {
62
+ height: 2;
63
+ padding: 0 1;
64
+ }
65
+
66
+ .component-row Checkbox {
67
+ width: 1fr;
68
+ }
69
+
70
+ #picker-buttons {
71
+ height: 3;
72
+ align: center middle;
73
+ background: #222;
74
+ }
75
+
76
+ #picker-buttons Button {
77
+ margin: 0 1;
78
+ background: #444;
79
+ color: #fff;
80
+ }
81
+
82
+ #picker-buttons Button:hover {
83
+ background: #666;
84
+ }
85
+
86
+ #picker-buttons Button:focus {
87
+ background: #555;
88
+ }
89
+
90
+ #picker-buttons #picker-confirm {
91
+ background: #1a4d8f;
92
+ }
93
+
94
+ #picker-buttons #picker-confirm:hover {
95
+ background: #2870c2;
96
+ }
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ title: str = "Select Application Components",
102
+ subtitle: str = "Choose the components for this project.",
103
+ allow_empty: bool = False,
104
+ ) -> None:
105
+ super().__init__()
106
+ self._title_text = title
107
+ self._subtitle_text = subtitle
108
+ self._allow_empty = allow_empty
109
+
110
+ def compose(self) -> ComposeResult:
111
+ with Vertical(id="picker-dialog"):
112
+ yield Static(self._title_text, id="picker-title")
113
+ yield Static(self._subtitle_text, id="picker-subtitle")
114
+ with VerticalScroll(id="picker-list"):
115
+ from cdh.scaffold import COMPONENTS
116
+
117
+ for spec in COMPONENTS:
118
+ label = f"{spec.label} \u2014 {spec.description}"
119
+ yield Checkbox(label, id=f"comp-{spec.id}", classes="component-row")
120
+ with Horizontal(id="picker-buttons"):
121
+ yield Button("Cancel", id="picker-cancel")
122
+ yield Button("Confirm", id="picker-confirm", variant="primary")
123
+ yield Footer()
124
+
125
+ def action_cancel(self) -> None:
126
+ self.dismiss(None)
127
+
128
+ @on(Button.Pressed, "#picker-cancel")
129
+ def on_cancel(self) -> None:
130
+ self.dismiss(None)
131
+
132
+ @on(Button.Pressed, "#picker-confirm")
133
+ def on_confirm(self) -> None:
134
+ selected: list[str] = []
135
+ from cdh.scaffold import COMPONENTS
136
+
137
+ for spec in COMPONENTS:
138
+ cb = self.query_one(f"#comp-{spec.id}", Checkbox)
139
+ if cb.value:
140
+ selected.append(spec.id)
141
+ if not selected and not self._allow_empty:
142
+ self.notify(
143
+ "Select at least one application component",
144
+ title="No components selected",
145
+ severity="error",
146
+ )
147
+ return
148
+ self.dismiss(selected)
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from functools import partial
2
3
  from pathlib import Path
3
4
  import random
@@ -8,10 +9,10 @@ from textual import getters
8
9
  from textual.binding import Binding
9
10
  from textual.command import Hit, Hits, Provider, DiscoveryHit
10
11
  from textual.content import Content
11
- from textual.events import ScreenResume, Click
12
+ from textual.events import ScreenResume
12
13
  from textual.screen import Screen
13
14
  from textual.reactive import var, reactive
14
- from textual.widgets import Footer, OptionList, DirectoryTree, Tree, Static
15
+ from textual.widgets import Footer, OptionList, DirectoryTree, Tree
15
16
  from textual import containers
16
17
  from textual.widget import Widget
17
18
 
@@ -29,7 +30,6 @@ from tui.widgets.conversation import Conversation
29
30
  from tui.widgets.project_directory_tree import ProjectDirectoryTree
30
31
  from tui.widgets.side_bar import SideBar
31
32
  from tui.widgets.session_tabs import SessionsTabs, SessionLabel
32
- from onecode.agent.cdh_loader import CdhProjectLoader
33
33
 
34
34
 
35
35
  class ModeProvider(Provider):
@@ -102,7 +102,7 @@ class MainScreen(Screen, can_focus=False):
102
102
  column = reactive(False)
103
103
  column_width = reactive(80)
104
104
  scrollbar = reactive("")
105
- project_path: var[Path] = var(Path("./").expanduser().absolute())
105
+ project_path: var[Path | None] = var(Path("./").expanduser().absolute())
106
106
 
107
107
  app = getters.app(A2TUIApp)
108
108
 
@@ -116,6 +116,7 @@ class MainScreen(Screen, can_focus=False):
116
116
  initial_prompt: str | None = None,
117
117
  ) -> None:
118
118
  super().__init__()
119
+ self._project_update_lock = asyncio.Lock()
119
120
  self.set_reactive(MainScreen.project_path, project_path)
120
121
  self._agent = agent
121
122
  self._agent_session_id = agent_session_id
@@ -154,30 +155,17 @@ class MainScreen(Screen, can_focus=False):
154
155
  id="context-panel",
155
156
  ),
156
157
  ]
157
- cdh_dir = CdhProjectLoader.find_cdh_dir(self.project_path)
158
- if cdh_dir is not None:
159
- panels.append(
160
- SideBar.Panel(
161
- "Project",
162
- ProjectDirectoryTree(
163
- self.project_path,
164
- id="project_directory_tree",
165
- ),
166
- flex=True,
167
- id="project-panel",
168
- ),
169
- )
170
- else:
171
- panels.append(
172
- SideBar.Panel(
173
- "No Project",
174
- Static(
175
- "[bold]No .cdh project[/]\n\nClick to initialize .cdh in this directory",
176
- id="init-cdh-hint",
177
- ),
178
- id="no-project-panel",
179
- ),
180
- )
158
+ # panels.append(
159
+ # SideBar.Panel(
160
+ # "Project",
161
+ # ProjectDirectoryTree(
162
+ # self.project_path,
163
+ # id="project_directory_tree",
164
+ # ),
165
+ # flex=True,
166
+ # id="project-panel",
167
+ # ),
168
+ # )
181
169
  panels.append(
182
170
  SideBar.Panel(
183
171
  "Modified Files",
@@ -217,12 +205,22 @@ class MainScreen(Screen, can_focus=False):
217
205
  self.post_message(messages.SessionNavigate(self.screen.id, +1))
218
206
 
219
207
  @on(messages.ProjectDirectoryUpdated)
220
- async def on_project_directory_update(self) -> None:
221
- self.project_path = self.app.project_dir
222
- sidebar = self.query_one(SideBar)
223
- cdh_dir = CdhProjectLoader.find_cdh_dir(self.project_path)
224
- if cdh_dir is not None:
225
- sidebar.remove_panel("no-project-panel")
208
+ async def on_project_directory_update(self, event: messages.ProjectDirectoryUpdated) -> None:
209
+ async with self._project_update_lock:
210
+ new_dir = event.project_dir
211
+ sidebar = self.query_one(SideBar)
212
+ if new_dir is None:
213
+ sidebar.remove_panel("project-panel")
214
+ self.project_path = None
215
+ if mf := sidebar.query_one_optional("#modified_files", ModifiedFiles):
216
+ mf.refresh_files()
217
+ self._swap_directory_watcher(None)
218
+ return
219
+ if self.project_path == new_dir and sidebar.has_panel("project-panel"):
220
+ if mf := sidebar.query_one_optional("#modified_files", ModifiedFiles):
221
+ mf.refresh_files()
222
+ return
223
+ self.project_path = new_dir
226
224
  if not sidebar.has_panel("project-panel"):
227
225
  collapsible = await sidebar.add_panel(
228
226
  SideBar.Panel(
@@ -238,62 +236,23 @@ class MainScreen(Screen, can_focus=False):
238
236
  tree = collapsible.query_one(ProjectDirectoryTree)
239
237
  tree.data_bind(path=MainScreen.project_path)
240
238
  tree.guide_depth = 3
241
- else:
242
- tree = sidebar.query_one(ProjectDirectoryTree)
243
- tree.path = self.project_path
244
- await tree.reload()
245
- else:
246
- sidebar.remove_panel("project-panel")
247
- if not sidebar.has_panel("no-project-panel"):
248
- await sidebar.add_panel(
249
- SideBar.Panel(
250
- "No Project",
251
- Static(
252
- "[bold]No .cdh project[/]\n\nClick to initialize .cdh in this directory",
253
- id="init-cdh-hint",
254
- ),
255
- id="no-project-panel",
256
- ),
257
- )
258
- if mf := sidebar.query_one_optional("#modified_files", ModifiedFiles):
259
- mf.refresh_files()
260
-
261
- @on(Click, "#init-cdh-hint")
262
- def on_init_cdh_click(self) -> None:
263
- from pathlib import Path
264
- from onecode.config_screen import EditFieldScreen
265
- from onecode.agent.cdh_loader import CdhProjectLoader
266
-
267
- default_path = str(self.project_path.resolve())
268
- self.app.push_screen(
269
- EditFieldScreen("Directory to initialize .cdh", default_path),
270
- self._on_init_cdh_path,
271
- )
239
+
240
+ if mf := sidebar.query_one_optional("#modified_files", ModifiedFiles):
241
+ mf.refresh_files()
242
+ self._swap_directory_watcher(new_dir)
272
243
 
273
- def _on_init_cdh_path(self, path_str: str | None) -> None:
274
- from pathlib import Path
275
- from onecode.agent.cdh_loader import CdhProjectLoader
244
+ def _swap_directory_watcher(self, project_path: Path | None) -> None:
245
+ try:
246
+ self.conversation.directory_watcher_swap(project_path)
247
+ except Exception:
248
+ pass
276
249
 
277
- if not path_str:
278
- return
250
+ def _has_project_panel(self) -> bool:
279
251
  try:
280
- target = Path(path_str).expanduser().resolve()
281
- if not target.is_dir():
282
- self.notify(f"Not a directory: {target}", severity="error")
283
- return
252
+ sidebar = self.query_one(SideBar)
284
253
  except Exception:
285
- self.notify("Invalid path", severity="error")
286
- return
287
- existing = CdhProjectLoader.find_cdh_dir(target)
288
- if existing is not None:
289
- self.notify(f".cdh already exists at {existing}", severity="warning")
290
- return
291
- name = target.name
292
- from cdh.scaffold import scaffold_dlc_project
293
- scaffold_dlc_project(target, name)
294
- CdhProjectLoader.init_project(target, name)
295
- self.notify(f"Initialized .cdh in {target}")
296
- self.post_message(messages.ProjectDirectoryUpdated())
254
+ return False
255
+ return sidebar.has_panel("project-panel")
297
256
 
298
257
  @on(DirectoryTree.FileSelected, "ProjectDirectoryTree")
299
258
  def on_project_directory_tree_selected(self, event: Tree.NodeSelected):
@@ -303,15 +262,8 @@ class MainScreen(Screen, can_focus=False):
303
262
  @on(acp_messages.Plan)
304
263
  async def on_acp_plan(self, message: acp_messages.Plan):
305
264
  message.stop()
306
- entries = [
307
- Plan.Entry(
308
- Content(entry["content"]),
309
- entry.get("priority", "medium"),
310
- entry.get("status", "pending"),
311
- )
312
- for entry in message.entries
313
- ]
314
- self.query_one("SideBar Plan", Plan).entries = entries
265
+ from tui.widgets.plan import entries_from_dicts
266
+ self.query_one("SideBar Plan", Plan).entries = entries_from_dicts(message.entries)
315
267
 
316
268
  @on(messages.SessionUpdate)
317
269
  async def on_session_update(self, event: messages.SessionUpdate) -> None:
@@ -20,6 +20,16 @@ ProjectsScreen {
20
20
  text-style: italic;
21
21
  }
22
22
 
23
+ #actions {
24
+ height: auto;
25
+ align-horizontal: center;
26
+ margin: 0 1 1 1;
27
+ }
28
+
29
+ #actions Button {
30
+ margin: 0 1;
31
+ }
32
+
23
33
 
24
34
  ProjectGridSelect {
25
35
  &:empty {