soothe-cli 0.6.14__tar.gz → 0.6.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/PKG-INFO +1 -1
  2. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -93
  3. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/config/cli_config.py +0 -36
  4. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/parse/tool_call_resolution.py +48 -5
  5. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_app.py +0 -4
  6. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_execution.py +0 -2
  7. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_messages_mixin.py +0 -5
  8. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_model.py +0 -42
  9. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/command_registry.py +0 -16
  10. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/preview_limits.py +1 -1
  11. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/sessions.py +1 -182
  12. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/textual_adapter.py +35 -2
  13. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/theme.py +0 -12
  14. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/unicode_security.py +0 -11
  15. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/autocomplete.py +0 -4
  16. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -268
  17. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/diff.py +3 -101
  18. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/loop_selector.py +0 -21
  19. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/__init__.py +81 -0
  20. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/_helpers.py +239 -0
  21. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/app.py +100 -0
  22. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/assistant.py +271 -0
  23. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/clarification.py +215 -0
  24. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +395 -0
  25. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/cognition_reason.py +111 -0
  26. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/cognition_step.py +1466 -0
  27. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +947 -0
  28. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/diff_message.py +110 -0
  29. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/error.py +59 -0
  30. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/skill.py +312 -0
  31. soothe_cli-0.6.16/src/soothe_cli/tui/widgets/messages/user.py +170 -0
  32. soothe_cli-0.6.14/src/soothe_cli/tui/widgets/messages.py +0 -3997
  33. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/.gitignore +0 -0
  34. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/README.md +0 -0
  35. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/pyproject.toml +0 -0
  36. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/__init__.py +0 -0
  37. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/__init__.py +0 -0
  38. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/commands/__init__.py +0 -0
  39. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
  40. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  41. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  42. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  43. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/execution/__init__.py +0 -0
  44. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/execution/daemon.py +0 -0
  45. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  46. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/execution/headless.py +0 -0
  47. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  48. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/execution/launcher.py +0 -0
  49. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/cli/main.py +0 -0
  50. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/config/__init__.py +0 -0
  51. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/config/loader.py +0 -0
  52. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/config/logging_setup.py +0 -0
  53. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/__init__.py +0 -0
  54. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/headless/processor.py +0 -0
  55. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  56. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/parse/_utils.py +0 -0
  57. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  58. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
  59. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  60. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  61. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  62. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  63. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  64. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  65. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  66. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  67. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  68. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  69. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  70. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/state/step_router.py +0 -0
  71. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  72. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/state/transcript.py +0 -0
  73. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/transport/session.py +0 -0
  74. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  75. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  76. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  77. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  78. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  79. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/runtime/wire/messages.py +0 -0
  80. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/__init__.py +0 -0
  81. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/_cli_context.py +0 -0
  82. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/_env_vars.py +0 -0
  83. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/_version.py +0 -0
  84. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/__init__.py +0 -0
  85. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_commands.py +0 -0
  86. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_history.py +0 -0
  87. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_module_init.py +0 -0
  88. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_startup.py +0 -0
  89. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/_ui.py +0 -0
  90. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/app/app.tcss +0 -0
  91. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/binding.py +0 -0
  92. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/commands/__init__.py +0 -0
  93. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/commands/command_router.py +0 -0
  94. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  95. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  96. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/config.py +0 -0
  97. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/file_change_notify.py +0 -0
  98. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  99. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/hooks.py +0 -0
  100. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/input.py +0 -0
  101. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/media_utils.py +0 -0
  102. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/model_config.py +0 -0
  103. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/path_utils.py +0 -0
  104. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/project_utils.py +0 -0
  105. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/skills/__init__.py +0 -0
  106. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/skills/invocation.py +0 -0
  107. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/skills/load.py +0 -0
  108. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/tips.py +0 -0
  109. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/tool_display.py +0 -0
  110. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/update_check.py +0 -0
  111. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  112. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/_links.py +0 -0
  113. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  114. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  115. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  116. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/editor.py +0 -0
  117. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  118. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/history.py +0 -0
  119. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/loading.py +0 -0
  120. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  121. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  122. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  123. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  124. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/status.py +0 -0
  125. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  126. {soothe_cli-0.6.14 → soothe_cli-0.6.16}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.6.14
3
+ Version: 0.6.16
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -397,96 +397,3 @@ def dream() -> None:
397
397
  client = _require_daemon_ws()
398
398
  client.autopilot_dream()
399
399
  typer.echo("Dream signal sent.")
400
-
401
-
402
- def _discover_goals(autopilot_dir: Path) -> list[dict]:
403
- """Parse goals from GOAL.md/GOALS.md files for offline CLI display."""
404
- import re
405
-
406
- goals: list[dict] = []
407
-
408
- goal_file = autopilot_dir / "GOAL.md"
409
- if goal_file.exists():
410
- g = _parse_single_goal(goal_file.read_text(), str(goal_file))
411
- if g:
412
- return [g]
413
-
414
- goals_file = autopilot_dir / "GOALS.md"
415
- if goals_file.exists():
416
- text = goals_file.read_text()
417
- for section in re.split(r"## Goal:", text)[1:]:
418
- g = _parse_goals_section(section.strip(), str(goals_file))
419
- if g:
420
- goals.append(g)
421
-
422
- goals_dir = autopilot_dir / "goals"
423
- if goals_dir.exists():
424
- for subdir in sorted(goals_dir.iterdir()):
425
- gfile = subdir / "GOAL.md"
426
- if gfile.exists():
427
- g = _parse_single_goal(gfile.read_text(), str(gfile))
428
- if g:
429
- goals.append(g)
430
-
431
- return goals
432
-
433
-
434
- def _parse_single_goal(text: str, source: str) -> dict | None:
435
- """Parse a single GOAL.md file."""
436
- if not text.startswith("---"):
437
- return None
438
-
439
- parts = text.split("---", 2)
440
- if len(parts) < 3: # noqa: PLR2004
441
- return None
442
-
443
- import yaml
444
-
445
- fm = yaml.safe_load(parts[1]) or {}
446
- body = parts[2].strip()
447
-
448
- desc = ""
449
- for line in body.splitlines():
450
- s = line.strip()
451
- if s.startswith("# "):
452
- desc = s[2:]
453
- break
454
-
455
- return {
456
- "id": fm.get("id", source.split("/")[-2]),
457
- "description": desc or preview_first(body, 100),
458
- "priority": int(fm.get("priority", 50)),
459
- "status": fm.get("status", "pending"),
460
- "depends_on": fm.get("depends_on", []),
461
- "source_file": source,
462
- }
463
-
464
-
465
- def _parse_goals_section(text: str, source: str) -> dict | None:
466
- """Parse a single goal section from GOALS.md."""
467
- lines = text.splitlines()
468
- name = lines[0].strip() if lines else ""
469
- metadata: dict = {}
470
-
471
- for line in lines[1:]:
472
- s = line.strip()
473
- if s.startswith("- id:"):
474
- metadata["id"] = s.split(":", 1)[1].strip()
475
- elif s.startswith("- priority:"):
476
- metadata["priority"] = int(s.split(":", 1)[1].strip())
477
- elif s.startswith("- depends_on:"):
478
- raw = s.split(":", 1)[1].strip()
479
- if raw.startswith("[") and raw.endswith("]"):
480
- inner = raw[1:-1].strip()
481
- metadata["depends_on"] = (
482
- [x.strip() for x in inner.split(",") if x.strip()] if inner else []
483
- )
484
-
485
- return {
486
- "id": metadata.get("id", name.lower().replace(" ", "-")),
487
- "description": name,
488
- "priority": metadata.get("priority", 50),
489
- "status": "pending",
490
- "depends_on": metadata.get("depends_on", []),
491
- "source_file": source,
492
- }
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
6
  from pathlib import Path
7
- from typing import Any
8
7
 
9
8
  from soothe_sdk.client.config import SOOTHE_HOME
10
9
 
@@ -44,41 +43,6 @@ class CLIConfig:
44
43
  # Paths
45
44
  soothe_home: Path = field(default_factory=lambda: Path(SOOTHE_HOME))
46
45
 
47
- # Daemon config cache (fetched via RPC)
48
- _daemon_config_cache: dict[str, Any] = field(default_factory=dict)
49
-
50
46
  def websocket_url(self) -> str:
51
47
  """Construct WebSocket URL for daemon connection."""
52
48
  return f"ws://{self.daemon_host}:{self.daemon_port}"
53
-
54
- async def fetch_daemon_config(self, section: str = "all") -> dict[str, Any]:
55
- """Fetch daemon config section via WebSocket RPC.
56
-
57
- Args:
58
- section: Config section name (e.g., "providers", "defaults", "all").
59
-
60
- Returns:
61
- Wire-safe config section dict.
62
- """
63
- from soothe_sdk.client import WebSocketClient, fetch_config_section
64
-
65
- client = WebSocketClient(url=self.websocket_url())
66
- await client.connect()
67
-
68
- try:
69
- config_section = await fetch_config_section(client, section, timeout=5.0)
70
- self._daemon_config_cache[section] = config_section
71
- return config_section
72
- finally:
73
- await client.close()
74
-
75
- def get_cached_config(self, section: str) -> dict[str, Any]:
76
- """Get cached daemon config section.
77
-
78
- Args:
79
- section: Config section name.
80
-
81
- Returns:
82
- Cached config section dict, or empty dict if not cached.
83
- """
84
- return self._daemon_config_cache.get(section, {})
@@ -64,6 +64,45 @@ def is_main_step_level_tool_call_id(tool_call_id: str) -> bool:
64
64
  return not is_step_level_task_tool_id(tcid)
65
65
 
66
66
 
67
+ def predict_main_execute_tool_call_id(
68
+ step_id: str,
69
+ *,
70
+ tool_call_id: str = "",
71
+ tool_name: str = "",
72
+ chunk_index: Any = None,
73
+ ) -> str:
74
+ """Best-effort unified id for main-graph execute tools during streaming."""
75
+ from soothe_sdk.ux.task_namespace import (
76
+ _format_unified_tool_call_id,
77
+ _provider_tool_fragment,
78
+ is_unified_tool_call_id,
79
+ parse_unified_tool_call_id,
80
+ )
81
+
82
+ tcid = str(tool_call_id or "").strip()
83
+ if tcid and is_unified_tool_call_id(tcid):
84
+ parsed_sid, type_code, _, _ = parse_unified_tool_call_id(tcid)
85
+ if parsed_sid and type_code == "s":
86
+ return tcid
87
+ sid = str(step_id or "").strip()
88
+ if not sid:
89
+ return tcid
90
+ raw_tid = tcid
91
+ if not raw_tid:
92
+ name = str(tool_name or "").strip()
93
+ if name and chunk_index is not None:
94
+ try:
95
+ idx = int(chunk_index)
96
+ except (TypeError, ValueError):
97
+ idx = None
98
+ if idx is not None:
99
+ raw_tid = f"{name}:{idx}"
100
+ if not raw_tid:
101
+ return ""
102
+ frag = _provider_tool_fragment(raw_tid)
103
+ return _format_unified_tool_call_id(sid, "s", frag)
104
+
105
+
67
106
  def is_task_level_subgraph_tool_call_id(tool_call_id: str) -> bool:
68
107
  """True for unified subgraph tools (``{step}:t{n}:{tool}:{seq}``), not nested ``task`` rows."""
69
108
  from soothe_sdk.ux.task_namespace import (
@@ -87,19 +126,22 @@ def should_ingest_tool_for_step_stats(
87
126
  ) -> bool:
88
127
  """Whether the TUI should register a tool row for step-card stats.
89
128
 
90
- Main step-level tools are tracked as soon as ``tool_call_id`` and ``tool_name``
91
- are known; display does not wait for streamed args.
129
+ Main execute-graph tools are tracked as soon as ``tool_call_id`` and ``tool_name``
130
+ are known; display does not wait for streamed args. Subgraph explore tools often
131
+ arrive with placeholder kwargs before real args.
92
132
  """
133
+ from soothe_sdk.ux.task_namespace import is_step_level_task_tool_id
134
+
93
135
  name = str(tool_name or "").strip()
94
136
  tcid = str(tool_call_id or "").strip()
95
137
  if not name or not tcid or name == "task":
96
138
  return False
97
139
  if args_meaningful:
98
140
  return True
99
- if is_step_card_scope and is_main_step_level_tool_call_id(tcid):
100
- return True
141
+ if is_step_card_scope:
142
+ return not is_step_level_task_tool_id(tcid)
101
143
  # Subgraph explore tools often arrive with ``{"_subgraph_tool": true}`` before real args.
102
- return not is_step_card_scope and is_task_level_subgraph_tool_call_id(tcid)
144
+ return is_task_level_subgraph_tool_call_id(tcid)
103
145
 
104
146
 
105
147
  def _args_from_toolish_block(block: dict[str, Any]) -> dict[str, Any]:
@@ -498,6 +540,7 @@ __all__ = [
498
540
  "is_main_step_level_tool_call_id",
499
541
  "is_step_card_tool_scope",
500
542
  "is_task_level_subgraph_tool_call_id",
543
+ "predict_main_execute_tool_call_id",
501
544
  "is_toolish_display_block",
502
545
  "materialize_ai_blocks_with_resolved_tools",
503
546
  "merge_tool_display_args",
@@ -49,10 +49,6 @@ from soothe_cli.tui.widgets.welcome import WelcomeBanner
49
49
  logger = logging.getLogger(__name__)
50
50
  _monotonic = time.monotonic
51
51
 
52
- InputMode = (
53
- "normal" # Literal type alias — actual value used in _module_init; here for isinstance guards
54
- )
55
-
56
52
 
57
53
  class SootheApp(
58
54
  App,
@@ -664,8 +664,6 @@ class _ExecutionMixin:
664
664
  await self._submit_cron_job(args, slash_input=command)
665
665
  elif cmd == "/autopilot-dashboard":
666
666
  await self._show_autopilot_dashboard()
667
- elif cmd == "/autopilot-toggle":
668
- await self._toggle_autopilot_mode()
669
667
  elif cmd == "/mcp":
670
668
  await self._show_mcp_viewer()
671
669
  elif cmd == "/theme":
@@ -743,8 +743,3 @@ class _MessagesMixin:
743
743
  # =========================================================================
744
744
  # Model Switching
745
745
  # =========================================================================
746
- # SOOTHE: Slash command actions
747
-
748
- def action_detach(self) -> None:
749
- """Exit TUI but leave daemon running."""
750
- self._detach_or_exit()
@@ -293,48 +293,6 @@ class _ModelMixin:
293
293
 
294
294
  self.push_screen(screen, handle_result)
295
295
 
296
- async def _toggle_autopilot_mode(self) -> None:
297
- """Toggle autopilot mode (solo ↔ autopilot) via daemon RPC.
298
-
299
- Sends command_request to daemon and updates local dashboard mode.
300
- """
301
- from soothe_cli.tui.widgets.messages import ErrorMessage, UserMessage
302
-
303
- await self._mount_message(UserMessage("/autopilot-toggle"))
304
-
305
- lid = self._loop_id
306
- if not lid:
307
- await self._mount_message(ErrorMessage("No active loop for autopilot toggle"))
308
- return
309
-
310
- if not self._client:
311
- await self._mount_message(ErrorMessage("Not connected to daemon"))
312
- return
313
-
314
- try:
315
- from soothe_cli.tui.commands.command_router import handle_rpc_command
316
- from soothe_cli.tui.commands.slash_commands import COMMANDS
317
-
318
- entry = COMMANDS.get("/autopilot-toggle")
319
- if not entry:
320
- await self._mount_message(ErrorMessage("Command /autopilot-toggle not found"))
321
- return
322
-
323
- await handle_rpc_command(
324
- entry,
325
- "autopilot-toggle",
326
- None,
327
- self._console,
328
- self._client,
329
- loop_id=lid,
330
- )
331
- new_mode = "solo" if self._loop_autopilot_mode == "autopilot" else "autopilot"
332
- self._apply_loop_autopilot_mode(new_mode)
333
-
334
- except Exception as exc:
335
- logger.exception("Autopilot toggle failed")
336
- await self._mount_message(ErrorMessage(f"Failed to toggle autopilot: {exc}"))
337
-
338
296
  async def _submit_autopilot_job(self, task: str) -> None:
339
297
  """Submit an autopilot job via WebSocket (like CLI `soothe autopilot run`).
340
298
 
@@ -222,22 +222,6 @@ IMMEDIATE_UI: frozenset[str] = _build_bypass_set(BypassTier.IMMEDIATE_UI)
222
222
  SIDE_EFFECT_FREE: frozenset[str] = _build_bypass_set(BypassTier.SIDE_EFFECT_FREE)
223
223
  """Commands whose side effect fires immediately; chat output deferred until idle."""
224
224
 
225
- QUEUE_BOUND: frozenset[str] = _build_bypass_set(BypassTier.QUEUED)
226
- """Commands that must wait in the queue when the app is busy."""
227
-
228
- HIDDEN_DEBUG: frozenset[str] = frozenset({"/debug-error"})
229
- """Hidden debug commands not exposed in autocomplete or help."""
230
-
231
- ALL_CLASSIFIED: frozenset[str] = (
232
- ALWAYS_IMMEDIATE
233
- | BYPASS_WHEN_CONNECTING
234
- | IMMEDIATE_UI
235
- | SIDE_EFFECT_FREE
236
- | QUEUE_BOUND
237
- | HIDDEN_DEBUG
238
- )
239
- """Union of all tiers plus hidden debug commands — used by drift tests."""
240
-
241
225
 
242
226
  # ---------------------------------------------------------------------------
243
227
  # Autocomplete tuples
@@ -17,7 +17,7 @@ STEP_CARD_SHOW_TOOL_ROW_DETAILS: Final[bool] = False
17
17
  # Latest per-tool invocation lines shown per scope (task branch vs main-agent branch).
18
18
  STEP_CARD_TOOL_ACTIVITY_PREVIEW_COUNT: Final[int] = 3
19
19
 
20
- # When estimated body lines exceed this count, the card auto-collapses (strict `>`).
20
+ # Optional manual full tool-list folding threshold (not auto-collapse).
21
21
  STEP_TASK_CARD_COLLAPSE_LINE_THRESHOLD: Final[int] = 3
22
22
 
23
23
  # --- Skill invocation cards (`SkillMessage` collapsed SKILL.md body) ---
@@ -3,71 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from contextlib import asynccontextmanager
7
6
  from datetime import datetime
8
- from pathlib import Path
9
- from typing import TYPE_CHECKING, Any
7
+ from typing import Any
10
8
 
11
- from soothe_sdk.client.config import SOOTHE_HOME
12
9
  from typing_extensions import TypedDict
13
10
 
14
- if TYPE_CHECKING:
15
- from collections.abc import AsyncIterator
16
-
17
- import aiosqlite
18
-
19
11
  logger = logging.getLogger(__name__)
20
12
 
21
- _aiosqlite_patched = False
22
-
23
-
24
- def _patch_aiosqlite() -> None:
25
- """Patch aiosqlite.Connection with `is_alive()` if missing.
26
-
27
- Required by langgraph-checkpoint>=2.1.0.
28
- See: https://github.com/langchain-ai/langgraph/issues/6583
29
- """
30
- global _aiosqlite_patched # noqa: PLW0603 # Module-level flag requires global statement
31
- if _aiosqlite_patched:
32
- return
33
-
34
- import aiosqlite as _aiosqlite
35
-
36
- if not hasattr(_aiosqlite.Connection, "is_alive"):
37
-
38
- def _is_alive(self: _aiosqlite.Connection) -> bool:
39
- """Check if the connection is still alive.
40
-
41
- Returns:
42
- True if connection is alive, False otherwise.
43
- """
44
- return bool(self._running and self._connection is not None)
45
-
46
- # Dynamically adding a method to aiosqlite.Connection at runtime.
47
- # Type checkers can't understand this monkey-patch, so we suppress the
48
- # "attr-defined" error that would otherwise be raised.
49
- _aiosqlite.Connection.is_alive = _is_alive # type: ignore[attr-defined]
50
-
51
- _aiosqlite_patched = True
52
-
53
-
54
- @asynccontextmanager
55
- async def _connect() -> AsyncIterator[aiosqlite.Connection]:
56
- """Import aiosqlite, apply the compatibility patch, and connect.
57
-
58
- Centralizes the deferred import + patch + connect sequence used by every
59
- database function in this module.
60
-
61
- Yields:
62
- An open aiosqlite connection to the sessions database.
63
- """
64
- import aiosqlite as _aiosqlite
65
-
66
- _patch_aiosqlite()
67
-
68
- async with _aiosqlite.connect(str(get_db_path()), timeout=30.0) as conn:
69
- yield conn
70
-
71
13
 
72
14
  class LoopInfo(TypedDict, total=False):
73
15
  """Loop metadata returned by `list_loops_via_daemon_rpc`."""
@@ -182,27 +124,6 @@ def format_relative_timestamp(iso_timestamp: str | None) -> str:
182
124
  return f"{years}y ago"
183
125
 
184
126
 
185
- _db_path: Path | None = None
186
-
187
-
188
- def get_db_path() -> Path:
189
- """Get path to global database.
190
-
191
- The result is cached after the first successful call to avoid repeated
192
- filesystem operations.
193
-
194
- Returns:
195
- Path to the SQLite database file.
196
- """
197
- global _db_path # noqa: PLW0603 # Module-level cache requires global statement
198
- if _db_path is not None:
199
- return _db_path
200
- db_dir = Path(SOOTHE_HOME)
201
- db_dir.mkdir(parents=True, exist_ok=True)
202
- _db_path = db_dir / "sessions.db"
203
- return _db_path
204
-
205
-
206
127
  def generate_loop_id() -> str:
207
128
  """Generate a new loop ID as a full UUID7 string.
208
129
 
@@ -214,17 +135,6 @@ def generate_loop_id() -> str:
214
135
  return str(uuid7())
215
136
 
216
137
 
217
- async def _table_exists(conn: aiosqlite.Connection, table: str) -> bool:
218
- """Check if a table exists in the database.
219
-
220
- Returns:
221
- True if table exists, False otherwise.
222
- """
223
- query = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
224
- async with conn.execute(query, (table,)) as cursor:
225
- return await cursor.fetchone() is not None
226
-
227
-
228
138
  async def list_loops_via_daemon_rpc(
229
139
  daemon_session: Any,
230
140
  limit: int = 20,
@@ -309,97 +219,6 @@ async def list_loops_via_daemon_rpc(
309
219
  return loops
310
220
 
311
221
 
312
- async def get_most_recent(agent_name: str | None = None) -> str | None:
313
- """Return the most recent loop id from local SQLite, optionally filtered by agent.
314
-
315
- Returns:
316
- Loop id (``checkpoints.thread_id``), or `None` when the database is empty.
317
- """
318
- async with _connect() as conn:
319
- if not await _table_exists(conn, "checkpoints"):
320
- return None
321
-
322
- if agent_name:
323
- query = """
324
- SELECT thread_id FROM checkpoints
325
- WHERE json_extract(metadata, '$.agent_name') = ?
326
- ORDER BY checkpoint_id DESC
327
- LIMIT 1
328
- """
329
- params: tuple = (agent_name,)
330
- else:
331
- query = "SELECT thread_id FROM checkpoints ORDER BY checkpoint_id DESC LIMIT 1"
332
- params = ()
333
-
334
- async with conn.execute(query, params) as cursor:
335
- row = await cursor.fetchone()
336
- return row[0] if row else None
337
-
338
-
339
- async def get_loop_agent(loop_id: str) -> str | None:
340
- """Return ``agent_name`` metadata for a loop id.
341
-
342
- Returns:
343
- Agent name from checkpoint row metadata, or `None` if not found.
344
- """
345
- async with _connect() as conn:
346
- if not await _table_exists(conn, "checkpoints"):
347
- return None
348
-
349
- query = """
350
- SELECT json_extract(metadata, '$.agent_name')
351
- FROM checkpoints
352
- WHERE thread_id = ?
353
- LIMIT 1
354
- """
355
- async with conn.execute(query, (loop_id,)) as cursor:
356
- row = await cursor.fetchone()
357
- return row[0] if row else None
358
-
359
-
360
- async def loop_exists(loop_id: str) -> bool:
361
- """Return True if any checkpoint row exists for this loop id.
362
-
363
- Returns:
364
- True if a checkpoint row exists, False otherwise.
365
- """
366
- async with _connect() as conn:
367
- if not await _table_exists(conn, "checkpoints"):
368
- return False
369
-
370
- query = "SELECT 1 FROM checkpoints WHERE thread_id = ? LIMIT 1"
371
- async with conn.execute(query, (loop_id,)) as cursor:
372
- row = await cursor.fetchone()
373
- return row is not None
374
-
375
-
376
- async def find_similar_loops(loop_id: str, limit: int = 3) -> list[str]:
377
- """Find loop ids that share the given prefix.
378
-
379
- Args:
380
- loop_id: Prefix to match against `checkpoints.thread_id`.
381
- limit: Maximum number of matches.
382
-
383
- Returns:
384
- Matching loop ids (SQLite ``thread_id`` column values).
385
- """
386
- async with _connect() as conn:
387
- if not await _table_exists(conn, "checkpoints"):
388
- return []
389
-
390
- query = """
391
- SELECT DISTINCT thread_id
392
- FROM checkpoints
393
- WHERE thread_id LIKE ?
394
- ORDER BY thread_id
395
- LIMIT ?
396
- """
397
- prefix = loop_id + "%"
398
- async with conn.execute(query, (prefix, limit)) as cursor:
399
- rows = await cursor.fetchall()
400
- return [r[0] for r in rows]
401
-
402
-
403
222
  def get_loop_limit() -> int:
404
223
  """Default maximum loops to load for `/resume` when no explicit limit is set.
405
224
 
@@ -66,6 +66,7 @@ from soothe_cli.runtime.parse.tool_call_resolution import (
66
66
  is_step_card_tool_scope,
67
67
  materialize_ai_blocks_with_resolved_tools,
68
68
  merge_tool_display_args,
69
+ predict_main_execute_tool_call_id,
69
70
  resolve_stream_tool_name,
70
71
  should_ingest_tool_for_step_stats,
71
72
  tool_args_meaningful,
@@ -983,6 +984,16 @@ async def apply_tool_call_wire_update(
983
984
  else:
984
985
  step_w.add_tool_call(tcid, name, update_payload, raw_args=raw_args_stream)
985
986
  adapter._tool_to_step[tcid] = step_w
987
+ elif name != "task":
988
+ update_payload = dict(display_args or {})
989
+ if not update_payload and raw_args_stream:
990
+ update_payload = {"_raw": raw_args_stream}
991
+ router.buffer_main_tool(
992
+ tcid,
993
+ name,
994
+ update_payload,
995
+ raw_args=raw_args_stream,
996
+ )
986
997
  return True
987
998
 
988
999
  _merge_buf, display_key = canonical_subgraph_tool_ids(ns_key, tcid, task_scope=ts)
@@ -2286,6 +2297,23 @@ async def execute_task_textual(
2286
2297
  continue
2287
2298
 
2288
2299
  lookup_id = str(buffer_id) if buffer_id is not None else ""
2300
+ if not lookup_id and is_step_scope and buffer_name:
2301
+ bound_step_id = ""
2302
+ if len(router.active_step_ids) == 1:
2303
+ bound_step_id = next(iter(router.active_step_ids))
2304
+ elif adapter._step_by_namespace.get(ns_key) is not None:
2305
+ bound_step_id = getattr(
2306
+ adapter._step_by_namespace[ns_key],
2307
+ "_step_id",
2308
+ "",
2309
+ )
2310
+ if bound_step_id:
2311
+ lookup_id = predict_main_execute_tool_call_id(
2312
+ bound_step_id,
2313
+ tool_call_id=lookup_id,
2314
+ tool_name=str(buffer_name),
2315
+ chunk_index=chunk_index,
2316
+ )
2289
2317
  raw_args_stream = ""
2290
2318
  pend_stream = (
2291
2319
  pending_tool_calls_lc.get(lookup_id) if lookup_id else None
@@ -2821,7 +2849,12 @@ async def execute_task_textual(
2821
2849
  data.get("assessment_reasoning", "")
2822
2850
  ).strip()
2823
2851
  plan_reasoning = str(data.get("plan_reasoning", "")).strip()
2824
- if not assessment_reasoning and not plan_reasoning:
2852
+ next_action = str(data.get("next_action", "")).strip()
2853
+ if (
2854
+ not assessment_reasoning
2855
+ and not plan_reasoning
2856
+ and not next_action
2857
+ ):
2825
2858
  continue
2826
2859
  pending_text = pending_text_by_namespace.get(ns_key, "")
2827
2860
  if pending_text:
@@ -2837,7 +2870,7 @@ async def execute_task_textual(
2837
2870
  pa_raw = data.get("plan_action", "")
2838
2871
  plan_action = pa_raw if pa_raw in ("keep", "new") else ""
2839
2872
  plan_widget = CognitionReasonMessage(
2840
- next_action="",
2873
+ next_action=next_action,
2841
2874
  status=str(data.get("status", "")),
2842
2875
  iteration=int(data.get("iteration", 0)),
2843
2876
  plan_action=str(plan_action),
@@ -99,9 +99,6 @@ LC_TOOL_HOVER = "#FFCB91"
99
99
  LC_COGNITION = "#56B6C2"
100
100
  """Agent loop step / cognition progress accent (distinct from tool amber)."""
101
101
 
102
- LC_COGNITION_HOVER = "#7BC9D3"
103
- """Cognition progress hover — lighter teal for interactive feedback."""
104
-
105
102
 
106
103
  # ---------------------------------------------------------------------------
107
104
  # Unified Card Palette — Grayscale system for Goal/Plan/Step/Task cards (IG-468)
@@ -194,9 +191,6 @@ CARD_ERROR_LIGHT = "#BE185D"
194
191
  LC_COGNITION = CARD_BORDER_DARK
195
192
  """Legacy alias — now uses unified card border color."""
196
193
 
197
- LC_COGNITION_HOVER = CARD_BORDER_HOVER_DARK
198
- """Legacy alias — now uses unified card hover color."""
199
-
200
194
 
201
195
  # ---------------------------------------------------------------------------
202
196
  # Brand palette — light
@@ -255,12 +249,6 @@ LC_LIGHT_TOOL = LC_LIGHT_AMBER
255
249
  LC_LIGHT_TOOL_HOVER = "#78350F"
256
250
  """Tool call hover (darkened for light bg contrast)."""
257
251
 
258
- LC_LIGHT_COGNITION = CARD_BORDER_LIGHT
259
- """Legacy alias — now uses unified card border color."""
260
-
261
- LC_LIGHT_COGNITION_HOVER = CARD_BORDER_HOVER_LIGHT
262
- """Legacy alias — now uses unified card hover color."""
263
-
264
252
 
265
253
  # ---------------------------------------------------------------------------
266
254
  # Semantic constants (ANSI color names for Rich console output)