@scemoon/cdh 1.0.0 → 1.0.4
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.
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/cli.py +473 -87
- package/cdh/scaffold.py +386 -121
- package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
- package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
- package/onecode/agent/agents/types.py +123 -79
- package/onecode/agent/cdh_loader.py +26 -13
- package/onecode/agent/engine.py +426 -235
- package/onecode/agent/onecode_agent_acp.py +202 -75
- package/onecode/agent/session.py +8 -13
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +4 -4
- package/onecode/agent/tools/todo_tools.py +188 -0
- package/onecode/cli.py +3 -3
- package/onecode/commands.py +439 -194
- package/onecode/config.py +3 -3
- package/onecode/config_screen.py +4 -2
- package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/errors.py +11 -1
- package/onecode/models/messages.py +35 -16
- package/onecode/models/provider.py +243 -12
- package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/anthropic_provider.py +4 -2
- package/onecode/models/providers/deepseek_provider.py +2 -0
- package/onecode/models/providers/glm_provider.py +2 -1
- package/onecode/models/providers/minimax_provider.py +2 -1
- package/onecode/models/providers/minimaxi_provider.py +76 -25
- package/onecode/models/providers/ollama_provider.py +2 -1
- package/onecode/models/providers/openai_provider.py +4 -0
- package/onecode/tasks/manager.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +10 -0
- package/run.js +24 -7
- package/tui/__pycache__/agents.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tui/__pycache__/db.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
- package/tui/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/acp/agent.py +60 -11
- package/tui/acp/api.py +6 -0
- package/tui/acp/messages.py +11 -0
- package/tui/acp/prompt.py +1 -1
- package/tui/acp/protocol.py +31 -0
- package/tui/agents.py +8 -7
- package/tui/app.py +60 -8
- package/tui/data/agents/ampcode.com.toml +1 -0
- package/tui/data/agents/augmentcode.com.toml +1 -0
- package/tui/data/agents/cdh.cloud-dev-harness.toml +3 -2
- package/tui/data/agents/crowai.dev.toml +1 -0
- package/tui/data/agents/deepagents.langchain.com +1 -0
- package/tui/data/agents/docker.com.toml +1 -0
- package/tui/data/agents/goose.ai.toml +1 -0
- package/tui/data/agents/inference.huggingface.co.toml +1 -1
- package/tui/data/agents/kiro.dev.toml +1 -0
- package/tui/data/agents/onecode.dev.toml +22 -0
- package/tui/data/agents/openclaw.ai.toml +1 -1
- package/tui/data/agents/openhands.dev.toml +1 -0
- package/tui/data/agents/stakpak.dev.toml +1 -0
- package/tui/data/agents/vibe.mistral.ai.toml +1 -0
- package/tui/data/agents/vtcode.dev.toml +1 -0
- package/tui/db.py +23 -0
- package/tui/directory_watcher.py +35 -1
- package/tui/messages.py +4 -1
- package/tui/paths.py +3 -3
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/component_picker.py +148 -0
- package/tui/screens/main.py +47 -95
- package/tui/screens/projects.tcss +10 -0
- package/tui/screens/projects_app.py +33 -5
- package/tui/screens/projects_screen.py +171 -9
- package/tui/screens/store.py +1 -1
- package/tui/settings_schema.py +1 -1
- package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/subagent.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/ask_user.py +1 -1
- package/tui/widgets/conversation.py +99 -42
- package/tui/widgets/modified_files.py +191 -65
- package/tui/widgets/plan.py +22 -0
- package/tui/widgets/project_directory_tree.py +22 -10
- package/tui/widgets/project_grid_select.py +23 -1
- package/tui/widgets/question.py +1 -1
- package/tui/widgets/subagent.py +202 -36
- package/tui/widgets/subagent_screen.py +186 -0
- package/tui/widgets/tool_call.py +5 -8
- package/cdh/__pycache__/__init__.cpython-313.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-313.pyc +0 -0
- package/onecode/__pycache__/config_screen.cpython-314.pyc +0 -0
- package/onecode/__pycache__/mock_data.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_agent_acp.cpython-313.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/harness_skill.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/task_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/task_tools.py +0 -249
- package/onecode/codebase/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/mock_provider.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/manager.cpython-314.pyc +0 -0
- package/tui/__pycache__/danger.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_suggester.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/log.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/projects_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/settings.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/store.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/acp_content.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-313.pyc +0 -0
- package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/directory_input.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/mandelbrot.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/markdown_note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_result.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-313.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-313.pyc +0 -0
|
@@ -267,6 +267,16 @@ This is a view of your conversation with the agent.
|
|
|
267
267
|
pass
|
|
268
268
|
|
|
269
269
|
|
|
270
|
+
def _plan_entries_from_dicts(raw_entries):
|
|
271
|
+
"""Deprecated thin wrapper around the shared ``entries_from_dicts`` helper.
|
|
272
|
+
|
|
273
|
+
Kept for backwards compatibility with any external callers; new code
|
|
274
|
+
should import ``entries_from_dicts`` directly from ``tui.widgets.plan``.
|
|
275
|
+
"""
|
|
276
|
+
from tui.widgets.plan import entries_from_dicts
|
|
277
|
+
return entries_from_dicts(raw_entries)
|
|
278
|
+
|
|
279
|
+
|
|
270
280
|
class Conversation(containers.Vertical):
|
|
271
281
|
"""Holds the agent conversation (input, output, and various controls / information)."""
|
|
272
282
|
|
|
@@ -378,6 +388,7 @@ class Conversation(containers.Vertical):
|
|
|
378
388
|
self._agent_thought: AgentThought | None = None
|
|
379
389
|
self._replay: bool = False
|
|
380
390
|
self._replay_buffer: list[dict] = []
|
|
391
|
+
self._active_subagents: dict = {}
|
|
381
392
|
self._last_escape_time: float = monotonic()
|
|
382
393
|
self._agent_data = agent
|
|
383
394
|
self._agent_session_id = agent_session_id
|
|
@@ -404,6 +415,7 @@ class Conversation(containers.Vertical):
|
|
|
404
415
|
|
|
405
416
|
self._directory_changed = False
|
|
406
417
|
self._directory_watcher: DirectoryWatcher | None = None
|
|
418
|
+
self._suppress_directory_changed = False
|
|
407
419
|
|
|
408
420
|
self._initial_prompt = initial_prompt
|
|
409
421
|
|
|
@@ -559,7 +571,34 @@ class Conversation(containers.Vertical):
|
|
|
559
571
|
@on(DirectoryChanged)
|
|
560
572
|
def on_directory_changed(self, event: DirectoryChanged) -> None:
|
|
561
573
|
event.stop()
|
|
562
|
-
self.
|
|
574
|
+
self._directory_changed = True
|
|
575
|
+
if self._suppress_directory_changed:
|
|
576
|
+
return
|
|
577
|
+
self._suppress_directory_changed = True
|
|
578
|
+
self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
|
|
579
|
+
self.set_timer(0.5, self._release_directory_changed_suppress)
|
|
580
|
+
|
|
581
|
+
def _release_directory_changed_suppress(self) -> None:
|
|
582
|
+
self._suppress_directory_changed = False
|
|
583
|
+
|
|
584
|
+
def directory_watcher_swap(self, project_path: Path) -> None:
|
|
585
|
+
"""Stop the current DirectoryWatcher (if any) and start a new one
|
|
586
|
+
rooted at *project_path*.
|
|
587
|
+
|
|
588
|
+
Used by ``MainScreen`` when the user switches projects so we don't
|
|
589
|
+
keep receiving events for the old tree.
|
|
590
|
+
"""
|
|
591
|
+
if self._directory_watcher is not None:
|
|
592
|
+
self._directory_watcher.stop()
|
|
593
|
+
self._directory_watcher = None
|
|
594
|
+
if project_path is None:
|
|
595
|
+
return
|
|
596
|
+
try:
|
|
597
|
+
watcher = DirectoryWatcher(project_path, self)
|
|
598
|
+
except Exception:
|
|
599
|
+
return
|
|
600
|
+
watcher.start()
|
|
601
|
+
self._directory_watcher = watcher
|
|
563
602
|
|
|
564
603
|
@on(Terminal.Finalized)
|
|
565
604
|
def on_terminal_finalized(self, event: Terminal.Finalized) -> None:
|
|
@@ -572,7 +611,7 @@ class Conversation(containers.Vertical):
|
|
|
572
611
|
if self._directory_changed or not self.is_watching_directory:
|
|
573
612
|
self.prompt.project_directory_updated()
|
|
574
613
|
self._directory_changed = False
|
|
575
|
-
self.post_message(messages.ProjectDirectoryUpdated())
|
|
614
|
+
self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
|
|
576
615
|
|
|
577
616
|
@on(Terminal.LongRunning)
|
|
578
617
|
def on_terminal_long_running(self, event: Terminal.LongRunning) -> None:
|
|
@@ -934,7 +973,7 @@ class Conversation(containers.Vertical):
|
|
|
934
973
|
|
|
935
974
|
if self._directory_changed or not self.is_watching_directory:
|
|
936
975
|
self._directory_changed = False
|
|
937
|
-
self.post_message(messages.ProjectDirectoryUpdated())
|
|
976
|
+
self.post_message(messages.ProjectDirectoryUpdated(project_dir=self.app.project_dir))
|
|
938
977
|
self.prompt.project_directory_updated()
|
|
939
978
|
|
|
940
979
|
self._turn_count += 1
|
|
@@ -1102,12 +1141,12 @@ class Conversation(containers.Vertical):
|
|
|
1102
1141
|
case "plan":
|
|
1103
1142
|
current_thought = None
|
|
1104
1143
|
self.new_block()
|
|
1105
|
-
widgets.append(Plan(entry["entries"]))
|
|
1144
|
+
widgets.append(Plan(_plan_entries_from_dicts(entry["entries"])))
|
|
1106
1145
|
|
|
1107
1146
|
case "subagent_start":
|
|
1108
1147
|
current_thought = None
|
|
1109
1148
|
self.new_block()
|
|
1110
|
-
sa = SubAgent(entry["agent_type"], tool_id=entry["id"])
|
|
1149
|
+
sa = SubAgent(entry["agent_type"], tool_id=entry["id"], prompt=entry.get("prompt", ""))
|
|
1111
1150
|
created_subagents[entry["id"]] = sa
|
|
1112
1151
|
widgets.append(sa)
|
|
1113
1152
|
|
|
@@ -1116,10 +1155,18 @@ class Conversation(containers.Vertical):
|
|
|
1116
1155
|
if sa is not None:
|
|
1117
1156
|
sa.append_chunk(entry["text"])
|
|
1118
1157
|
|
|
1158
|
+
case "subagent_thinking":
|
|
1159
|
+
sa = created_subagents.get(entry["id"])
|
|
1160
|
+
if sa is not None:
|
|
1161
|
+
sa.append_thinking(entry["text"])
|
|
1162
|
+
|
|
1119
1163
|
case "subagent_end":
|
|
1120
1164
|
sa = created_subagents.get(entry["id"])
|
|
1121
1165
|
if sa is not None:
|
|
1122
|
-
sa.complete(
|
|
1166
|
+
sa.complete(
|
|
1167
|
+
entry.get("status", "completed"),
|
|
1168
|
+
entry.get("error", ""),
|
|
1169
|
+
)
|
|
1123
1170
|
|
|
1124
1171
|
case "ask_user":
|
|
1125
1172
|
current_thought = None
|
|
@@ -1192,14 +1239,7 @@ class Conversation(containers.Vertical):
|
|
|
1192
1239
|
return
|
|
1193
1240
|
from tui.widgets.plan import Plan
|
|
1194
1241
|
|
|
1195
|
-
entries =
|
|
1196
|
-
Plan.Entry(
|
|
1197
|
-
Content(entry["content"]),
|
|
1198
|
-
entry.get("priority", "medium"),
|
|
1199
|
-
entry.get("status", "pending"),
|
|
1200
|
-
)
|
|
1201
|
-
for entry in message.entries
|
|
1202
|
-
]
|
|
1242
|
+
entries = _plan_entries_from_dicts(message.entries)
|
|
1203
1243
|
|
|
1204
1244
|
# Look up any existing Plan widget anywhere in the conversation
|
|
1205
1245
|
# tree — checking only ``children[-1]`` would create duplicates if
|
|
@@ -1310,46 +1350,62 @@ class Conversation(containers.Vertical):
|
|
|
1310
1350
|
|
|
1311
1351
|
message.stop()
|
|
1312
1352
|
if self._replay:
|
|
1313
|
-
self._replay_buffer.append({"kind": "subagent_start", "agent_type": message.agent_type, "id": message.subagent_id})
|
|
1353
|
+
self._replay_buffer.append({"kind": "subagent_start", "agent_type": message.agent_type, "id": message.subagent_id, "prompt": message.prompt})
|
|
1314
1354
|
return
|
|
1315
|
-
sa = SubAgent(message.agent_type, tool_id=message.subagent_id)
|
|
1355
|
+
sa = SubAgent(message.agent_type, tool_id=message.subagent_id, prompt=message.prompt)
|
|
1356
|
+
self._active_subagents[message.subagent_id] = sa
|
|
1316
1357
|
await self.post(sa, new_block=True)
|
|
1317
1358
|
|
|
1318
|
-
|
|
1319
|
-
async def on_acp_subagent_chunk(self, message: acp_messages.SubAgentChunk):
|
|
1359
|
+
def _get_subagent(self, subagent_id: str):
|
|
1320
1360
|
from tui.widgets.subagent import SubAgent
|
|
1321
1361
|
|
|
1362
|
+
sa = self._active_subagents.get(subagent_id)
|
|
1363
|
+
if sa is not None:
|
|
1364
|
+
return sa
|
|
1365
|
+
try:
|
|
1366
|
+
return self.contents.get_child_by_id(subagent_id, SubAgent)
|
|
1367
|
+
except NoMatches:
|
|
1368
|
+
return None
|
|
1369
|
+
|
|
1370
|
+
@on(acp_messages.SubAgentChunk)
|
|
1371
|
+
async def on_acp_subagent_chunk(self, message: acp_messages.SubAgentChunk):
|
|
1322
1372
|
message.stop()
|
|
1323
1373
|
if self._replay:
|
|
1324
1374
|
self._replay_buffer.append({"kind": "subagent_chunk", "id": message.subagent_id, "text": message.text})
|
|
1325
1375
|
return
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1376
|
+
sa = self._get_subagent(message.subagent_id)
|
|
1377
|
+
if sa is not None:
|
|
1378
|
+
sa.append_chunk(message.text)
|
|
1379
|
+
|
|
1380
|
+
@on(acp_messages.SubAgentThinking)
|
|
1381
|
+
async def on_acp_subagent_thinking(self, message: acp_messages.SubAgentThinking):
|
|
1382
|
+
message.stop()
|
|
1383
|
+
if self._replay:
|
|
1384
|
+
self._replay_buffer.append({
|
|
1385
|
+
"kind": "subagent_thinking",
|
|
1386
|
+
"id": message.subagent_id,
|
|
1387
|
+
"text": message.text,
|
|
1388
|
+
})
|
|
1331
1389
|
return
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1390
|
+
sa = self._get_subagent(message.subagent_id)
|
|
1391
|
+
if sa is not None:
|
|
1392
|
+
sa.append_thinking(message.text)
|
|
1335
1393
|
|
|
1336
1394
|
@on(acp_messages.SubAgentEnd)
|
|
1337
1395
|
async def on_acp_subagent_end(self, message: acp_messages.SubAgentEnd):
|
|
1338
|
-
from tui.widgets.subagent import SubAgent
|
|
1339
|
-
|
|
1340
1396
|
message.stop()
|
|
1341
1397
|
if self._replay:
|
|
1342
|
-
self._replay_buffer.append({
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
message.
|
|
1347
|
-
)
|
|
1348
|
-
except NoMatches:
|
|
1398
|
+
self._replay_buffer.append({
|
|
1399
|
+
"kind": "subagent_end",
|
|
1400
|
+
"id": message.subagent_id,
|
|
1401
|
+
"status": message.status,
|
|
1402
|
+
"error": message.error,
|
|
1403
|
+
})
|
|
1349
1404
|
return
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1405
|
+
sa = self._get_subagent(message.subagent_id)
|
|
1406
|
+
if sa is not None:
|
|
1407
|
+
sa.complete(message.status, message.error)
|
|
1408
|
+
self._active_subagents.pop(message.subagent_id, None)
|
|
1353
1409
|
|
|
1354
1410
|
@on(acp_messages.AvailableCommandsUpdate)
|
|
1355
1411
|
async def on_acp_available_commands_update(
|
|
@@ -1929,12 +1985,13 @@ class Conversation(containers.Vertical):
|
|
|
1929
1985
|
self.cursor_offset = -1
|
|
1930
1986
|
self.cursor.visible = False
|
|
1931
1987
|
self.cursor.follow(None)
|
|
1932
|
-
contents.refresh(
|
|
1988
|
+
contents.refresh()
|
|
1933
1989
|
|
|
1934
1990
|
if prune_children:
|
|
1935
1991
|
await contents.remove_children(prune_children)
|
|
1936
1992
|
|
|
1937
|
-
self.
|
|
1993
|
+
if self.window.scroll_y >= self.window.max_scroll_y:
|
|
1994
|
+
self.call_later(self.window.anchor)
|
|
1938
1995
|
|
|
1939
1996
|
async def new_terminal(self) -> Terminal:
|
|
1940
1997
|
"""Create a new interactive Terminal.
|
|
@@ -2401,7 +2458,7 @@ class Conversation(containers.Vertical):
|
|
|
2401
2458
|
save_config(cfg)
|
|
2402
2459
|
new_project_dir = Path(project_path) if project_path else Path.cwd()
|
|
2403
2460
|
self.app.project_dir = new_project_dir
|
|
2404
|
-
self.post_message(messages.ProjectDirectoryUpdated())
|
|
2461
|
+
self.post_message(messages.ProjectDirectoryUpdated(project_dir=new_project_dir))
|
|
2405
2462
|
self.flash(f"Switched to project: {name}", style="success")
|
|
2406
2463
|
return True
|
|
2407
2464
|
elif sub_cmd == "new":
|
|
@@ -2427,7 +2484,7 @@ class Conversation(containers.Vertical):
|
|
|
2427
2484
|
cfg.current_project_path = str(ws)
|
|
2428
2485
|
save_config(cfg)
|
|
2429
2486
|
self.app.project_dir = ws
|
|
2430
|
-
self.post_message(messages.ProjectDirectoryUpdated())
|
|
2487
|
+
self.post_message(messages.ProjectDirectoryUpdated(project_dir=ws))
|
|
2431
2488
|
self.flash(f"Created and switched to project: {name}", style="success")
|
|
2432
2489
|
return True
|
|
2433
2490
|
else:
|
|
@@ -3,48 +3,74 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import subprocess
|
|
5
5
|
|
|
6
|
+
from textual import work
|
|
6
7
|
from textual.app import ComposeResult
|
|
7
8
|
from textual.reactive import reactive
|
|
9
|
+
from textual.timer import Timer
|
|
8
10
|
from textual.widgets import Static
|
|
9
11
|
from textual import containers
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
DEFAULT_CSS = """
|
|
14
|
-
ModifiedFiles {
|
|
15
|
-
height: auto;
|
|
16
|
-
max-height: 15;
|
|
17
|
-
overflow-y: auto;
|
|
14
|
+
CONTENT_WIDTH = 35
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
16
|
+
GIT_STATUS_TIMEOUT = 5.0
|
|
17
|
+
GIT_STATUS_DEBOUNCE = 0.2
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
NOT_A_REPO_TEXT = "Not a git repository"
|
|
20
|
+
NO_CHANGES_TEXT = "No modified files"
|
|
21
|
+
LOADING_TEXT = "Checking…"
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
IGNORED_DIRS = frozenset(
|
|
24
|
+
{
|
|
25
|
+
".venv",
|
|
26
|
+
".git",
|
|
27
|
+
".pytest_cache",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
".mypy_cache",
|
|
30
|
+
"node_modules",
|
|
31
|
+
".uv-cache",
|
|
32
|
+
"dist",
|
|
33
|
+
"build",
|
|
34
|
+
".idea",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
STATUS_ICONS = {"?": "?", "M": "M", "A": "A", "D": "D", "R": "R", "C": "C"}
|
|
39
|
+
STATUS_COLORS = {"?": "green", "M": "yellow", "A": "green", "D": "red", "R": "#888888", "C": "green"}
|
|
40
|
+
DIFF_COLORS: dict[str, str] = {"add": "green", "del": "red", "mix": "yellow", "dim": "#555555"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _status_info(raw: str) -> tuple[str, str]:
|
|
44
|
+
ch = raw.strip()[:1] or "M"
|
|
45
|
+
return STATUS_ICONS.get(ch, "M"), STATUS_COLORS.get(ch, "yellow")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _tag_color(added: int, deleted: int) -> str:
|
|
49
|
+
if added and deleted:
|
|
50
|
+
return DIFF_COLORS["mix"]
|
|
51
|
+
if added:
|
|
52
|
+
return DIFF_COLORS["add"]
|
|
53
|
+
if deleted:
|
|
54
|
+
return DIFF_COLORS["del"]
|
|
55
|
+
return DIFF_COLORS["dim"]
|
|
34
56
|
|
|
35
|
-
.file-renamed {
|
|
36
|
-
color: $text-secondary;
|
|
37
|
-
}
|
|
38
57
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
class ModifiedFiles(containers.Vertical):
|
|
59
|
+
DEFAULT_CSS = """
|
|
60
|
+
ModifiedFiles {
|
|
61
|
+
height: auto;
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
padding: 0 0 0 0;
|
|
64
|
+
|
|
65
|
+
#mf-files {
|
|
66
|
+
height: auto;
|
|
67
|
+
padding: 0 0 0 1;
|
|
42
68
|
}
|
|
43
69
|
}
|
|
44
70
|
"""
|
|
45
71
|
|
|
46
72
|
path: reactive[Path | None] = reactive(None)
|
|
47
|
-
files: reactive[list[str]] = reactive(
|
|
73
|
+
files: reactive[list[str]] = reactive(list, recompose=False)
|
|
48
74
|
|
|
49
75
|
def __init__(
|
|
50
76
|
self,
|
|
@@ -54,53 +80,153 @@ class ModifiedFiles(containers.Vertical):
|
|
|
54
80
|
classes: str | None = None,
|
|
55
81
|
) -> None:
|
|
56
82
|
super().__init__(name=name, id=id, classes=classes)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
self._debounce_timer: Timer | None = None
|
|
84
|
+
self._status_kind: str = "no-changes"
|
|
85
|
+
self._status_text: str = NO_CHANGES_TEXT
|
|
86
|
+
self._initial_path: Path | None = path
|
|
87
|
+
|
|
88
|
+
def on_mount(self) -> None:
|
|
89
|
+
initial = self._initial_path
|
|
90
|
+
self._initial_path = None
|
|
91
|
+
if initial is not None:
|
|
92
|
+
self.path = initial
|
|
60
93
|
|
|
61
94
|
def watch_path(self, path: Path | None) -> None:
|
|
62
|
-
|
|
95
|
+
self._cancel_debounce()
|
|
63
96
|
if path is None:
|
|
64
|
-
self.
|
|
97
|
+
self._show_status("no-changes", NO_CHANGES_TEXT)
|
|
65
98
|
return
|
|
99
|
+
self._show_status("-loading", LOADING_TEXT)
|
|
100
|
+
self._schedule_run(path)
|
|
101
|
+
|
|
102
|
+
def refresh_files(self) -> None:
|
|
103
|
+
if self.path is not None:
|
|
104
|
+
self._cancel_debounce()
|
|
105
|
+
self._show_status("-loading", LOADING_TEXT)
|
|
106
|
+
self._run_git_status(self.path)
|
|
107
|
+
|
|
108
|
+
def _schedule_run(self, path: Path) -> None:
|
|
109
|
+
timer = self.set_timer(
|
|
110
|
+
GIT_STATUS_DEBOUNCE,
|
|
111
|
+
lambda p=path: self._run_git_status(p),
|
|
112
|
+
)
|
|
113
|
+
self._debounce_timer = timer
|
|
114
|
+
|
|
115
|
+
def _cancel_debounce(self) -> None:
|
|
116
|
+
timer = self._debounce_timer
|
|
117
|
+
if timer is not None:
|
|
118
|
+
try:
|
|
119
|
+
timer.stop()
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
self._debounce_timer = None
|
|
123
|
+
|
|
124
|
+
@work(exclusive=True, group="modified-files-git-status", thread=True)
|
|
125
|
+
def _run_git_status(self, path: Path) -> None:
|
|
126
|
+
app = self.app
|
|
127
|
+
outcome = self._do_git_status(path)
|
|
128
|
+
if outcome is None:
|
|
129
|
+
return
|
|
130
|
+
kind, text, lines = outcome
|
|
131
|
+
if lines is None:
|
|
132
|
+
app.call_from_thread(self._show_status, kind, text)
|
|
133
|
+
else:
|
|
134
|
+
app.call_from_thread(self._render_lines, lines)
|
|
135
|
+
|
|
136
|
+
def _do_git_status(
|
|
137
|
+
self, path: Path
|
|
138
|
+
) -> tuple[str, str, list[str] | None] | None:
|
|
139
|
+
if any(part in IGNORED_DIRS for part in path.parts):
|
|
140
|
+
return ("not-a-repo", NOT_A_REPO_TEXT, None)
|
|
66
141
|
try:
|
|
67
|
-
|
|
142
|
+
status = subprocess.run(
|
|
68
143
|
["git", "-C", str(path), "status", "--porcelain"],
|
|
69
|
-
capture_output=True,
|
|
70
|
-
|
|
71
|
-
timeout=5,
|
|
144
|
+
capture_output=True, text=True,
|
|
145
|
+
timeout=GIT_STATUS_TIMEOUT,
|
|
72
146
|
)
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
except subprocess.TimeoutExpired:
|
|
148
|
+
return ("no-changes", NO_CHANGES_TEXT, None)
|
|
149
|
+
except FileNotFoundError:
|
|
150
|
+
return ("not-a-repo", NOT_A_REPO_TEXT, None)
|
|
151
|
+
except Exception:
|
|
152
|
+
return ("no-changes", NO_CHANGES_TEXT, None)
|
|
153
|
+
if status.returncode != 0:
|
|
154
|
+
return ("not-a-repo", NOT_A_REPO_TEXT, None)
|
|
155
|
+
|
|
156
|
+
lines = [l for l in status.stdout.splitlines() if l.strip()]
|
|
157
|
+
if not lines:
|
|
158
|
+
return ("no-changes", NO_CHANGES_TEXT, ([], {}))
|
|
159
|
+
|
|
160
|
+
diffmap: dict[str, tuple[int, int]] = {}
|
|
161
|
+
for diff_cmd in (
|
|
162
|
+
["git", "-C", str(path), "diff", "--numstat"],
|
|
163
|
+
["git", "-C", str(path), "diff", "--cached", "--numstat"],
|
|
164
|
+
):
|
|
165
|
+
try:
|
|
166
|
+
r = subprocess.run(
|
|
167
|
+
diff_cmd, capture_output=True, text=True,
|
|
168
|
+
timeout=GIT_STATUS_TIMEOUT,
|
|
169
|
+
)
|
|
170
|
+
except Exception:
|
|
171
|
+
continue
|
|
172
|
+
for dl in r.stdout.splitlines():
|
|
173
|
+
parts = dl.split("\t", 2)
|
|
174
|
+
if len(parts) == 3:
|
|
175
|
+
added, deleted = parts[0], parts[1]
|
|
176
|
+
if added != "-":
|
|
177
|
+
a, d = int(added), int(deleted)
|
|
178
|
+
fp = parts[2]
|
|
179
|
+
prev_a, prev_d = diffmap.get(fp, (0, 0))
|
|
180
|
+
diffmap[fp] = (prev_a + a, prev_d + d)
|
|
181
|
+
|
|
182
|
+
return ("file-modified", "", (lines, diffmap))
|
|
183
|
+
|
|
184
|
+
def _render_lines(self, data: tuple[list[str], dict[str, tuple[int, int]]]) -> None:
|
|
185
|
+
lines, diffmap = data
|
|
186
|
+
self.files = list(lines)
|
|
187
|
+
if not lines:
|
|
188
|
+
self._show_status("no-changes", NO_CHANGES_TEXT)
|
|
189
|
+
return
|
|
190
|
+
self._show_status("file-modified", "")
|
|
191
|
+
|
|
192
|
+
out: list[str] = []
|
|
193
|
+
for raw_line in lines:
|
|
194
|
+
filepath = raw_line[3:]
|
|
195
|
+
icon, sc = _status_info(raw_line[:2])
|
|
196
|
+
|
|
197
|
+
added, deleted = diffmap.get(filepath, (0, 0))
|
|
198
|
+
if "?" in raw_line[:2]:
|
|
199
|
+
tag = "new"
|
|
200
|
+
tc = DIFF_COLORS["dim"]
|
|
201
|
+
elif "D" in raw_line[:2]:
|
|
202
|
+
tag = "del"
|
|
203
|
+
tc = DIFF_COLORS["del"]
|
|
204
|
+
elif added or deleted:
|
|
205
|
+
tag = f"+{added}/-{deleted}"
|
|
206
|
+
tc = _tag_color(added, deleted)
|
|
75
207
|
else:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
208
|
+
tag = ""
|
|
209
|
+
tc = ""
|
|
210
|
+
|
|
211
|
+
tag_s = f" [{tc}]{tag}[/]" if tag else ""
|
|
212
|
+
tag_len = len(tag) + 1 if tag else 0
|
|
213
|
+
max_fn = CONTENT_WIDTH - 2 - tag_len
|
|
214
|
+
if max_fn < 3:
|
|
215
|
+
max_fn = 3
|
|
216
|
+
disp = filepath[:max_fn - 1] + "…" if len(filepath) > max_fn else filepath
|
|
217
|
+
pad = CONTENT_WIDTH - 2 - len(disp) - tag_len
|
|
218
|
+
pad_s = " " * pad if pad > 0 else ""
|
|
219
|
+
out.append(f"[{sc}]{icon} {disp}[/]{pad_s}{tag_s}")
|
|
220
|
+
|
|
221
|
+
files_widget = self.query_one("#mf-files", Static)
|
|
222
|
+
files_widget.update("\n".join(out))
|
|
223
|
+
|
|
224
|
+
def _show_status(self, kind: str, text: str) -> None:
|
|
225
|
+
self._status_kind = kind
|
|
226
|
+
self._status_text = text
|
|
227
|
+
status = self.query_one("#mf-status", Static)
|
|
228
|
+
status.update(text)
|
|
83
229
|
|
|
84
230
|
def compose(self) -> ComposeResult:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
for line in self.files:
|
|
89
|
-
status = line[:2]
|
|
90
|
-
filepath = line[3:]
|
|
91
|
-
cls = self._class_for_status(status)
|
|
92
|
-
yield Static(f" {filepath}", classes=cls)
|
|
93
|
-
|
|
94
|
-
@staticmethod
|
|
95
|
-
def _class_for_status(status: str) -> str:
|
|
96
|
-
if "?" in status:
|
|
97
|
-
return "file-added"
|
|
98
|
-
if "D" in status:
|
|
99
|
-
return "file-deleted"
|
|
100
|
-
if "A" in status or "C" in status:
|
|
101
|
-
return "file-added"
|
|
102
|
-
if "R" in status:
|
|
103
|
-
return "file-renamed"
|
|
104
|
-
if "M" in status:
|
|
105
|
-
return "file-modified"
|
|
106
|
-
return "file-modified"
|
|
231
|
+
yield Static(self._status_text, id="mf-status")
|
|
232
|
+
yield Static("", id="mf-files")
|
package/tui/widgets/plan.py
CHANGED
|
@@ -186,6 +186,28 @@ class Plan(containers.Grid):
|
|
|
186
186
|
return Content()
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def entries_from_dicts(raw_entries):
|
|
190
|
+
"""Convert raw plan entries (dicts) into ``Plan.Entry`` dataclass instances.
|
|
191
|
+
|
|
192
|
+
Shared by ``MainScreen`` (sidebar) and ``Conversation`` (inline) so both
|
|
193
|
+
Plan widgets render the same data the same way. Filters entries with
|
|
194
|
+
empty content so the Plan widget never renders a blank row.
|
|
195
|
+
"""
|
|
196
|
+
result = []
|
|
197
|
+
for entry in raw_entries:
|
|
198
|
+
content = (entry.get("content") or "").strip()
|
|
199
|
+
if not content:
|
|
200
|
+
continue
|
|
201
|
+
result.append(
|
|
202
|
+
Plan.Entry(
|
|
203
|
+
Content(content),
|
|
204
|
+
entry.get("priority", "medium"),
|
|
205
|
+
entry.get("status", "pending"),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
189
211
|
if __name__ == "__main__":
|
|
190
212
|
from textual.app import App
|
|
191
213
|
|
|
@@ -42,26 +42,37 @@ This shows the files in your project directory.
|
|
|
42
42
|
disabled: bool = False,
|
|
43
43
|
) -> None:
|
|
44
44
|
self.path_filter: PathFilter | None = None
|
|
45
|
+
self._reload_lock = asyncio.Lock()
|
|
45
46
|
path = Path(path).resolve() if isinstance(path, str) else path.resolve()
|
|
46
47
|
super().__init__(path, name=name, id=id, classes=classes, disabled=disabled)
|
|
47
48
|
|
|
49
|
+
async def _update_path_filter(self) -> None:
|
|
50
|
+
if self.path is None:
|
|
51
|
+
self.path_filter = None
|
|
52
|
+
return
|
|
53
|
+
path = Path(self.path) if isinstance(self.path, str) else self.path
|
|
54
|
+
path = await asyncio.to_thread(path.resolve)
|
|
55
|
+
self.path_filter = await asyncio.to_thread(PathFilter.from_git_root, path)
|
|
56
|
+
|
|
48
57
|
async def watch_path(self) -> None:
|
|
49
58
|
"""Watch for changes to the `path` of the directory tree.
|
|
50
59
|
|
|
51
60
|
If the path is changed the directory tree will be repopulated using
|
|
52
61
|
the new value as the root.
|
|
53
62
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
63
|
+
if self.path is None:
|
|
64
|
+
return
|
|
65
|
+
async with self._reload_lock:
|
|
66
|
+
has_cursor = self.cursor_node is not None
|
|
67
|
+
self.reset_node(self.root, str(self.path), DirEntry(self.PATH(self.path)))
|
|
68
|
+
await self._update_path_filter()
|
|
69
|
+
await self.reload()
|
|
70
|
+
if has_cursor:
|
|
71
|
+
self.cursor_line = 0
|
|
72
|
+
self.scroll_to(0, 0, animate=False)
|
|
60
73
|
|
|
61
74
|
async def on_mount(self) -> None:
|
|
62
|
-
|
|
63
|
-
path = await asyncio.to_thread(path.resolve)
|
|
64
|
-
self.path_filter = await asyncio.to_thread(PathFilter.from_git_root, path)
|
|
75
|
+
await self._update_path_filter()
|
|
65
76
|
|
|
66
77
|
def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:
|
|
67
78
|
"""Filter the paths before adding them to the tree.
|
|
@@ -86,5 +97,6 @@ This shows the files in your project directory.
|
|
|
86
97
|
|
|
87
98
|
@work
|
|
88
99
|
async def action_refresh(self) -> None:
|
|
89
|
-
|
|
100
|
+
async with self._reload_lock:
|
|
101
|
+
await self.reload()
|
|
90
102
|
self.notify("Project directory has been refreshed", title="Directory Tree")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
+
import yaml
|
|
3
4
|
from textual.app import ComposeResult
|
|
4
5
|
from textual import getters
|
|
5
6
|
|
|
@@ -9,6 +10,24 @@ from tui.widgets.project_summary import ProjectSummary
|
|
|
9
10
|
from onecode.config import CLOUD_DEV_HARNESS_DIR
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def _read_project_path(pf: Path) -> str:
|
|
14
|
+
"""Return the project directory path stored in ``pf``, or the
|
|
15
|
+
yaml/json file path as a fallback."""
|
|
16
|
+
try:
|
|
17
|
+
if pf.suffix == ".json":
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
data = json.loads(pf.read_text(encoding="utf-8")) or {}
|
|
21
|
+
else:
|
|
22
|
+
data = yaml.safe_load(pf.read_text(encoding="utf-8")) or {}
|
|
23
|
+
path = data.get("path")
|
|
24
|
+
if isinstance(path, str) and path:
|
|
25
|
+
return path
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
return str(pf)
|
|
29
|
+
|
|
30
|
+
|
|
12
31
|
class ProjectGridSelect(GridSelect):
|
|
13
32
|
FOCUS_ON_CLICK = True
|
|
14
33
|
app: getters.app[A2TUIApp] = getters.app(A2TUIApp)
|
|
@@ -32,4 +51,7 @@ class ProjectGridSelect(GridSelect):
|
|
|
32
51
|
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
33
52
|
project_files = list(projects_dir.glob("*.yaml")) + list(projects_dir.glob("*.json"))
|
|
34
53
|
for pf in sorted(project_files):
|
|
35
|
-
yield ProjectSummary(pf.stem,
|
|
54
|
+
yield ProjectSummary(pf.stem, _read_project_path(pf), id=pf.stem)
|
|
55
|
+
|
|
56
|
+
def refresh(self, **kwargs) -> None:
|
|
57
|
+
super().refresh(**kwargs)
|