soothe-cli 0.4.3__tar.gz → 0.4.5__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.
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/.gitignore +1 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/PKG-INFO +1 -1
- soothe_cli-0.4.5/src/soothe_cli/__init__.py +10 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/main.py +1 -1
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/config_loader.py +1 -1
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py +33 -1
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/_env_vars.py +1 -4
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/app.py +3 -92
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/command_registry.py +0 -5
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/config.py +15 -258
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/model_config.py +6 -6
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/messages.py +11 -3
- soothe_cli-0.4.3/src/soothe_cli/__init__.py +0 -5
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/README.md +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/pyproject.toml +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/commands/thread_cmd.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/execution/daemon.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/stream/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/stream/context.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/stream/display_line.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/stream/formatter.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/stream/pipeline.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/cli/stream/task_scope.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/commands/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/commands/command_router.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/commands/slash_commands.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/commands/subagent_routing.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/core/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/core/event_processor.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/core/presentation_engine.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/core/processor_state.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/core/renderer_protocol.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/events/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/events/display_policy.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/events/essential_events.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/events/explore_task_display.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/events/stream_accumulator.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/events/tui_trace_log.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/presentation_engine.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/renderer_base.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/rendering/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/rendering/async_renderer_protocol.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/rendering/renderer_base.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/stream_accumulator.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/subagent_routing.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/message_processing.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/rendering.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_call_resolution.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_card_payload.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_card_visibility.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/base.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/execution.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/fallback.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/media.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/structured.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/subagent.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/web.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_message_format.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_output_formatter.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/app.tcss +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/daemon_session.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/tools.py +0 -0
- {soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Main CLI entry point using Typer."""
|
|
2
2
|
|
|
3
3
|
# Load environment variables from .env file BEFORE any langchain imports
|
|
4
|
-
#
|
|
4
|
+
# so provider API keys and other env-backed config are visible at import time.
|
|
5
5
|
from dotenv import load_dotenv
|
|
6
6
|
|
|
7
7
|
load_dotenv()
|
|
@@ -29,7 +29,7 @@ def load_config(config_path: str | None = None) -> CLIConfig:
|
|
|
29
29
|
A ``CLIConfig`` instance.
|
|
30
30
|
"""
|
|
31
31
|
# Load environment variables from .env file
|
|
32
|
-
# This ensures
|
|
32
|
+
# This ensures API keys and other env vars from .env are available
|
|
33
33
|
load_dotenv()
|
|
34
34
|
|
|
35
35
|
if config_path is not None:
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/file_ops.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from soothe_cli.shared.tools.tool_formatters.base import BaseFormatter
|
|
@@ -12,7 +13,7 @@ class FileOpsFormatter(BaseFormatter):
|
|
|
12
13
|
"""Formatter for file operation tools.
|
|
13
14
|
|
|
14
15
|
Handles: read_file, write_file, delete_file, list_files, search_files, glob,
|
|
15
|
-
grep, ls
|
|
16
|
+
grep, ls, file_info
|
|
16
17
|
|
|
17
18
|
Provides semantic summaries with size, line count, and item count metrics.
|
|
18
19
|
"""
|
|
@@ -54,9 +55,40 @@ class FileOpsFormatter(BaseFormatter):
|
|
|
54
55
|
return self._format_glob(result)
|
|
55
56
|
if normalized == "grep":
|
|
56
57
|
return self._format_search_files(result)
|
|
58
|
+
if normalized == "file_info":
|
|
59
|
+
return self._format_file_info(result)
|
|
57
60
|
msg = f"Unknown file operation tool: {tool_name}"
|
|
58
61
|
raise ValueError(msg)
|
|
59
62
|
|
|
63
|
+
_SIZE_BYTES_LINE = re.compile(r"Size:\s*(\d+)\s*bytes", re.IGNORECASE)
|
|
64
|
+
|
|
65
|
+
def _format_file_info(self, result: str) -> ToolBrief:
|
|
66
|
+
"""Format file_info metadata result (path, size, mtime, …)."""
|
|
67
|
+
if result.startswith("Error:"):
|
|
68
|
+
error_msg = result[6:].strip()
|
|
69
|
+
return ToolBrief(
|
|
70
|
+
icon="✗",
|
|
71
|
+
summary="File info failed",
|
|
72
|
+
detail=self._truncate_text(error_msg, 80),
|
|
73
|
+
metrics={"error": True},
|
|
74
|
+
)
|
|
75
|
+
m = self._SIZE_BYTES_LINE.search(result)
|
|
76
|
+
if m:
|
|
77
|
+
size_bytes = int(m.group(1))
|
|
78
|
+
size_str = self._format_size(size_bytes)
|
|
79
|
+
return ToolBrief(
|
|
80
|
+
icon="✓",
|
|
81
|
+
summary=f"Metadata {size_str}",
|
|
82
|
+
detail=None,
|
|
83
|
+
metrics={"size_bytes": size_bytes},
|
|
84
|
+
)
|
|
85
|
+
return ToolBrief(
|
|
86
|
+
icon="✓",
|
|
87
|
+
summary="File metadata",
|
|
88
|
+
detail=None,
|
|
89
|
+
metrics={},
|
|
90
|
+
)
|
|
91
|
+
|
|
60
92
|
def _format_read_file(self, result: str) -> ToolBrief:
|
|
61
93
|
r"""Format read_file result.
|
|
62
94
|
|
|
@@ -40,9 +40,6 @@ DEBUG_FILE = "SOOTHE_CLI_DEBUG_FILE"
|
|
|
40
40
|
EXTRA_SKILLS_DIRS = "SOOTHE_CLI_EXTRA_SKILLS_DIRS"
|
|
41
41
|
"""Colon-separated paths added to the skill containment allowlist."""
|
|
42
42
|
|
|
43
|
-
LANGSMITH_PROJECT = "SOOTHE_CLI_LANGSMITH_PROJECT"
|
|
44
|
-
"""Override LangSmith project name for agent traces."""
|
|
45
|
-
|
|
46
43
|
NO_UPDATE_CHECK = "SOOTHE_CLI_NO_UPDATE_CHECK"
|
|
47
44
|
"""Disable automatic update checking when set."""
|
|
48
45
|
|
|
@@ -53,4 +50,4 @@ SHELL_ALLOW_LIST = "SOOTHE_CLI_SHELL_ALLOW_LIST"
|
|
|
53
50
|
"""Comma-separated shell commands to allow (or 'recommended'/'all')."""
|
|
54
51
|
|
|
55
52
|
USER_ID = "SOOTHE_CLI_USER_ID"
|
|
56
|
-
"""Attach a user identifier to
|
|
53
|
+
"""Attach a user identifier to stream metadata (when set)."""
|
|
@@ -2640,104 +2640,17 @@ class SootheApp(App):
|
|
|
2640
2640
|
|
|
2641
2641
|
@staticmethod
|
|
2642
2642
|
async def _build_thread_message(prefix: str, thread_id: str) -> str | Content:
|
|
2643
|
-
"""Build a thread status message
|
|
2644
|
-
|
|
2645
|
-
Attempts to resolve the LangSmith thread URL with a short timeout.
|
|
2646
|
-
Falls back to plain text if tracing is not configured or resolution
|
|
2647
|
-
fails.
|
|
2643
|
+
"""Build a thread status message with the thread id.
|
|
2648
2644
|
|
|
2649
2645
|
Args:
|
|
2650
2646
|
prefix: Label before the thread ID (e.g. `'Resumed thread'`).
|
|
2651
2647
|
thread_id: The thread identifier.
|
|
2652
2648
|
|
|
2653
2649
|
Returns:
|
|
2654
|
-
|
|
2650
|
+
Plain status line.
|
|
2655
2651
|
"""
|
|
2656
|
-
from soothe_cli.tui.config import build_langsmith_thread_url
|
|
2657
|
-
|
|
2658
|
-
try:
|
|
2659
|
-
url = await asyncio.wait_for(
|
|
2660
|
-
asyncio.to_thread(build_langsmith_thread_url, thread_id),
|
|
2661
|
-
timeout=2.0,
|
|
2662
|
-
)
|
|
2663
|
-
except (TimeoutError, Exception): # noqa: BLE001 # Resilient non-interactive mode error handling
|
|
2664
|
-
url = None
|
|
2665
|
-
|
|
2666
|
-
if url:
|
|
2667
|
-
return Content.assemble(
|
|
2668
|
-
f"{prefix}: ",
|
|
2669
|
-
(thread_id, TStyle(link=url)),
|
|
2670
|
-
)
|
|
2671
2652
|
return f"{prefix}: {thread_id}"
|
|
2672
2653
|
|
|
2673
|
-
async def _handle_trace_command(self, command: str) -> None:
|
|
2674
|
-
"""Open the current thread in LangSmith.
|
|
2675
|
-
|
|
2676
|
-
Resolves the URL and opens the browser immediately regardless of busy
|
|
2677
|
-
state. When the app is busy, chat output (user echo + clickable link)
|
|
2678
|
-
is deferred until the current task finishes. Error conditions (no
|
|
2679
|
-
session, URL failure, tracing not configured) render immediately
|
|
2680
|
-
regardless of busy state.
|
|
2681
|
-
|
|
2682
|
-
Args:
|
|
2683
|
-
command: The raw command text (displayed as user message).
|
|
2684
|
-
"""
|
|
2685
|
-
from soothe_cli.tui.config import build_langsmith_thread_url
|
|
2686
|
-
|
|
2687
|
-
if not self._session_state:
|
|
2688
|
-
await self._mount_message(UserMessage(command))
|
|
2689
|
-
await self._mount_message(AppMessage("No active session."))
|
|
2690
|
-
return
|
|
2691
|
-
thread_id = self._session_state.loop_id
|
|
2692
|
-
try:
|
|
2693
|
-
url = await asyncio.to_thread(build_langsmith_thread_url, thread_id)
|
|
2694
|
-
except Exception:
|
|
2695
|
-
logger.exception("Failed to build LangSmith thread URL for %s", thread_id)
|
|
2696
|
-
await self._mount_message(UserMessage(command))
|
|
2697
|
-
await self._mount_message(AppMessage("Failed to resolve LangSmith thread URL."))
|
|
2698
|
-
return
|
|
2699
|
-
if not url:
|
|
2700
|
-
await self._mount_message(UserMessage(command))
|
|
2701
|
-
await self._mount_message(
|
|
2702
|
-
AppMessage(
|
|
2703
|
-
"LangSmith tracing is not configured. Set LANGSMITH_API_KEY and LANGSMITH_TRACING=true to enable."
|
|
2704
|
-
)
|
|
2705
|
-
)
|
|
2706
|
-
return
|
|
2707
|
-
|
|
2708
|
-
def _open_browser() -> None:
|
|
2709
|
-
try:
|
|
2710
|
-
webbrowser.open(url)
|
|
2711
|
-
except Exception:
|
|
2712
|
-
logger.debug("Could not open browser for URL: %s", url, exc_info=True)
|
|
2713
|
-
|
|
2714
|
-
asyncio.get_running_loop().run_in_executor(None, _open_browser)
|
|
2715
|
-
|
|
2716
|
-
# Defer chat output while a turn is in progress — rendering the user
|
|
2717
|
-
# echo + link immediately would splice it into the middle of the
|
|
2718
|
-
# streaming assistant response
|
|
2719
|
-
if self._agent_running or self._shell_running:
|
|
2720
|
-
queued_widget = QueuedUserMessage(command)
|
|
2721
|
-
self._queued_widgets.append(queued_widget)
|
|
2722
|
-
await self._mount_message(queued_widget)
|
|
2723
|
-
|
|
2724
|
-
async def _mount_output() -> None:
|
|
2725
|
-
if queued_widget in self._queued_widgets:
|
|
2726
|
-
self._queued_widgets.remove(queued_widget)
|
|
2727
|
-
with suppress(Exception):
|
|
2728
|
-
await queued_widget.remove()
|
|
2729
|
-
await self._mount_message(UserMessage(command))
|
|
2730
|
-
link = Content.styled(url, TStyle(dim=True, italic=True, link=url))
|
|
2731
|
-
await self._mount_message(AppMessage(link))
|
|
2732
|
-
|
|
2733
|
-
# Append directly — no dedup; each /trace invocation gets its own output.
|
|
2734
|
-
self._deferred_actions.append(DeferredAction(kind="chat_output", execute=_mount_output))
|
|
2735
|
-
return
|
|
2736
|
-
|
|
2737
|
-
await self._mount_message(UserMessage(command))
|
|
2738
|
-
link = Content.styled(url, TStyle(dim=True, italic=True, link=url))
|
|
2739
|
-
await self._mount_message(AppMessage(link))
|
|
2740
|
-
|
|
2741
2654
|
async def _handle_command(self, command: str) -> None:
|
|
2742
2655
|
"""Handle a slash command.
|
|
2743
2656
|
|
|
@@ -2785,7 +2698,7 @@ class SootheApp(App):
|
|
|
2785
2698
|
"Commands: /quit, /clear, /editor, /autopilot, /mcp, "
|
|
2786
2699
|
"/model [--model-params JSON] [--default], /notifications, "
|
|
2787
2700
|
"/reload, /skill:<name>, /remember, /theme, "
|
|
2788
|
-
"/tokens, /loops,
|
|
2701
|
+
"/tokens, /loops, "
|
|
2789
2702
|
"/browser, /claude, /research, /explore, /plan (subagent routing), "
|
|
2790
2703
|
"/update, /auto-update, /changelog, /docs, /feedback, /help\n\n"
|
|
2791
2704
|
"Interactive Features:\n"
|
|
@@ -2869,8 +2782,6 @@ class SootheApp(App):
|
|
|
2869
2782
|
await self.action_open_editor()
|
|
2870
2783
|
elif cmd == "/loops":
|
|
2871
2784
|
await self._show_loop_selector()
|
|
2872
|
-
elif cmd == "/trace":
|
|
2873
|
-
await self._handle_trace_command(command)
|
|
2874
2785
|
elif cmd == "/update":
|
|
2875
2786
|
await self._handle_update_command()
|
|
2876
2787
|
elif cmd == "/auto-update":
|
|
@@ -100,11 +100,6 @@ COMMANDS: tuple[SlashCommand, ...] = (
|
|
|
100
100
|
bypass_tier=BypassTier.IMMEDIATE_UI,
|
|
101
101
|
hidden_keywords="continue history sessions",
|
|
102
102
|
),
|
|
103
|
-
SlashCommand(
|
|
104
|
-
name="/trace",
|
|
105
|
-
description="Open current thread in LangSmith",
|
|
106
|
-
bypass_tier=BypassTier.SIDE_EFFECT_FREE,
|
|
107
|
-
),
|
|
108
103
|
SlashCommand(
|
|
109
104
|
name="/browser",
|
|
110
105
|
description="Route prompt to Browser subagent (usage: /browser <query>)",
|
|
@@ -24,10 +24,10 @@ from soothe_cli.tui._version import __version__
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
# ---------------------------------------------------------------------------
|
|
27
|
-
# Lazy bootstrap: dotenv loading
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
27
|
+
# Lazy bootstrap: dotenv loading and start-path detection are deferred until
|
|
28
|
+
# first access of `settings` (via module `__getattr__`). This avoids disk I/O
|
|
29
|
+
# and path traversal during import for callers that never touch `settings`
|
|
30
|
+
# (e.g. `Soothe --help`).
|
|
31
31
|
# ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
_bootstrap_done = False
|
|
@@ -43,13 +43,6 @@ _singleton_lock = threading.Lock()
|
|
|
43
43
|
_bootstrap_start_path: Path | None = None
|
|
44
44
|
"""Working directory captured at bootstrap time for dotenv and project discovery."""
|
|
45
45
|
|
|
46
|
-
_original_langsmith_project: str | None = None
|
|
47
|
-
"""Caller's `LANGSMITH_PROJECT` value before the CLI overrides it for agent traces.
|
|
48
|
-
|
|
49
|
-
Captured inside `_ensure_bootstrap()` after dotenv loading but before the
|
|
50
|
-
`LANGSMITH_PROJECT` override, so `.env`-only values are visible.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
46
|
|
|
54
47
|
def _find_dotenv_from_start_path(start_path: Path) -> Path | None:
|
|
55
48
|
"""Find the nearest `.env` file from an explicit start path upward.
|
|
@@ -149,7 +142,7 @@ def _load_dotenv(*, start_path: Path | None = None) -> bool:
|
|
|
149
142
|
|
|
150
143
|
|
|
151
144
|
def _ensure_bootstrap() -> None:
|
|
152
|
-
"""Run one-time bootstrap: dotenv loading and
|
|
145
|
+
"""Run one-time bootstrap: dotenv loading from project and global paths.
|
|
153
146
|
|
|
154
147
|
Idempotent and thread-safe — subsequent calls are no-ops. Called
|
|
155
148
|
automatically by `_get_settings()` when `settings` is first accessed.
|
|
@@ -159,7 +152,7 @@ def _ensure_bootstrap() -> None:
|
|
|
159
152
|
loops. Exceptions are caught and logged at ERROR level; the CLI proceeds
|
|
160
153
|
with the environment as-is.
|
|
161
154
|
"""
|
|
162
|
-
global _bootstrap_done, _bootstrap_start_path
|
|
155
|
+
global _bootstrap_done, _bootstrap_start_path # noqa: PLW0603
|
|
163
156
|
|
|
164
157
|
if _bootstrap_done:
|
|
165
158
|
return
|
|
@@ -176,54 +169,10 @@ def _ensure_bootstrap() -> None:
|
|
|
176
169
|
ctx = _get_server_project_context()
|
|
177
170
|
_bootstrap_start_path = ctx.user_cwd if ctx else None
|
|
178
171
|
_load_dotenv(start_path=_bootstrap_start_path)
|
|
179
|
-
|
|
180
|
-
# Capture AFTER dotenv loading so .env-only values are visible,
|
|
181
|
-
# but BEFORE the override below replaces it.
|
|
182
|
-
_original_langsmith_project = os.environ.get("LANGSMITH_PROJECT")
|
|
183
|
-
|
|
184
|
-
# CRITICAL: Override LANGSMITH_PROJECT to route agent traces to a
|
|
185
|
-
# separate project. LangSmith reads LANGSMITH_PROJECT at invocation
|
|
186
|
-
# time, so we override it here and preserve the user's original
|
|
187
|
-
# value for shell commands.
|
|
188
|
-
from soothe_cli.tui._env_vars import LANGSMITH_PROJECT
|
|
189
|
-
|
|
190
|
-
soothe_project = os.environ.get(LANGSMITH_PROJECT)
|
|
191
|
-
if soothe_project:
|
|
192
|
-
os.environ["LANGSMITH_PROJECT"] = soothe_project
|
|
193
|
-
|
|
194
|
-
# Propagate prefixed LangSmith env vars to canonical names.
|
|
195
|
-
# The CLI resolves prefixed vars via resolve_env_var(), but the
|
|
196
|
-
# LangSmith SDK reads os.environ directly and has no knowledge
|
|
197
|
-
# of the SOOTHE_ prefix. Setting canonical vars here
|
|
198
|
-
# bridges that gap.
|
|
199
|
-
from soothe_cli.tui.model_config import _ENV_PREFIX
|
|
200
|
-
|
|
201
|
-
for canonical in (
|
|
202
|
-
"LANGSMITH_API_KEY",
|
|
203
|
-
"LANGCHAIN_API_KEY",
|
|
204
|
-
"LANGSMITH_TRACING",
|
|
205
|
-
"LANGCHAIN_TRACING_V2",
|
|
206
|
-
):
|
|
207
|
-
prefixed = f"{_ENV_PREFIX}{canonical}"
|
|
208
|
-
if prefixed not in os.environ:
|
|
209
|
-
continue
|
|
210
|
-
prefixed_val = os.environ[prefixed]
|
|
211
|
-
if canonical not in os.environ:
|
|
212
|
-
# Propagate (including empty string for explicit disable).
|
|
213
|
-
os.environ[canonical] = prefixed_val
|
|
214
|
-
elif os.environ[canonical] != prefixed_val:
|
|
215
|
-
os.environ[canonical] = prefixed_val
|
|
216
|
-
logger.warning(
|
|
217
|
-
"Both %s and %s are set with different values; using %s. Unset %s to silence this warning.",
|
|
218
|
-
canonical,
|
|
219
|
-
prefixed,
|
|
220
|
-
prefixed,
|
|
221
|
-
canonical,
|
|
222
|
-
)
|
|
223
172
|
except Exception:
|
|
224
173
|
logger.exception(
|
|
225
|
-
"Bootstrap failed; .env
|
|
226
|
-
"
|
|
174
|
+
"Bootstrap failed; project .env may not be loaded. "
|
|
175
|
+
"The CLI will proceed with environment as-is.",
|
|
227
176
|
)
|
|
228
177
|
finally:
|
|
229
178
|
_bootstrap_done = True
|
|
@@ -375,15 +324,6 @@ _glyphs_cache: Glyphs | None = None
|
|
|
375
324
|
_editable_cache: tuple[bool, str | None] | None = None
|
|
376
325
|
"""Module-level cache for editable install info: (is_editable, source_path)."""
|
|
377
326
|
|
|
378
|
-
_langsmith_url_cache: tuple[str, str] | None = None
|
|
379
|
-
"""Module-level cache for successful LangSmith project URL lookups."""
|
|
380
|
-
|
|
381
|
-
_LANGSMITH_URL_LOOKUP_TIMEOUT_SECONDS = 2.0
|
|
382
|
-
"""Max seconds to wait for LangSmith project URL lookup.
|
|
383
|
-
|
|
384
|
-
Kept short so tracing metadata can never stall CLI flows.
|
|
385
|
-
"""
|
|
386
|
-
|
|
387
327
|
|
|
388
328
|
def _resolve_editable_info() -> tuple[bool, str | None]:
|
|
389
329
|
"""Parse PEP 610 `direct_url.json` once and cache both results.
|
|
@@ -613,8 +553,8 @@ def build_stream_config(
|
|
|
613
553
|
) -> RunnableConfig:
|
|
614
554
|
"""Build the LangGraph stream config dict.
|
|
615
555
|
|
|
616
|
-
Injects CLI and SDK versions into `metadata["versions"]` so
|
|
617
|
-
|
|
556
|
+
Injects CLI and SDK versions into `metadata["versions"]` so runs can be
|
|
557
|
+
correlated with specific releases.
|
|
618
558
|
|
|
619
559
|
Why the CLI sets *both* versions:
|
|
620
560
|
|
|
@@ -627,9 +567,6 @@ def build_stream_config(
|
|
|
627
567
|
version would be lost.
|
|
628
568
|
* Including the SDK version here ensures it survives the merge.
|
|
629
569
|
|
|
630
|
-
Includes `ls_integration` metadata so LangSmith traces originating from the CLI
|
|
631
|
-
are distinguishable from bare SDK usage.
|
|
632
|
-
|
|
633
570
|
Args:
|
|
634
571
|
thread_id: The CLI session thread identifier.
|
|
635
572
|
assistant_id: The agent/assistant identifier, if any.
|
|
@@ -660,7 +597,6 @@ def build_stream_config(
|
|
|
660
597
|
|
|
661
598
|
metadata: dict[str, Any] = {
|
|
662
599
|
"versions": versions,
|
|
663
|
-
"ls_integration": "Soothe",
|
|
664
600
|
}
|
|
665
601
|
from soothe_cli.tui._env_vars import USER_ID
|
|
666
602
|
|
|
@@ -883,12 +819,6 @@ class Settings:
|
|
|
883
819
|
google_cloud_project: str | None
|
|
884
820
|
"""Google Cloud project ID for VertexAI authentication."""
|
|
885
821
|
|
|
886
|
-
soothe_langchain_project: str | None
|
|
887
|
-
"""LangSmith project name for Soothe agent tracing."""
|
|
888
|
-
|
|
889
|
-
user_langchain_project: str | None
|
|
890
|
-
"""Original `LANGSMITH_PROJECT` from environment (for user code)."""
|
|
891
|
-
|
|
892
822
|
model_name: str | None = None
|
|
893
823
|
"""Currently active model name, set after model creation."""
|
|
894
824
|
|
|
@@ -940,23 +870,11 @@ class Settings:
|
|
|
940
870
|
tavily_key = resolve_env_var("TAVILY_API_KEY")
|
|
941
871
|
google_cloud_project = resolve_env_var("GOOGLE_CLOUD_PROJECT")
|
|
942
872
|
|
|
943
|
-
# Detect LangSmith configuration
|
|
944
|
-
# SOOTHE_CLI_LANGSMITH_PROJECT: Project for Soothe agent tracing
|
|
945
|
-
# user_langchain_project: User's ORIGINAL LANGSMITH_PROJECT (before override)
|
|
946
|
-
# When accessed via the module-level `settings` singleton,
|
|
947
|
-
# _ensure_bootstrap() has already run and may have overridden
|
|
948
|
-
# LANGSMITH_PROJECT. We use the saved original value, not the
|
|
949
|
-
# current os.environ value. Direct callers should ensure
|
|
950
|
-
# bootstrap has run if they depend on the override.
|
|
951
873
|
from soothe_cli.tui._env_vars import (
|
|
952
874
|
EXTRA_SKILLS_DIRS,
|
|
953
|
-
LANGSMITH_PROJECT,
|
|
954
875
|
SHELL_ALLOW_LIST,
|
|
955
876
|
)
|
|
956
877
|
|
|
957
|
-
soothe_langchain_project = resolve_env_var(LANGSMITH_PROJECT)
|
|
958
|
-
user_langchain_project = _original_langsmith_project # Use saved original!
|
|
959
|
-
|
|
960
878
|
# Detect project
|
|
961
879
|
from soothe_cli.tui.project_utils import find_project_root
|
|
962
880
|
|
|
@@ -983,8 +901,6 @@ class Settings:
|
|
|
983
901
|
nvidia_api_key=nvidia_key,
|
|
984
902
|
tavily_api_key=tavily_key,
|
|
985
903
|
google_cloud_project=google_cloud_project,
|
|
986
|
-
soothe_langchain_project=soothe_langchain_project,
|
|
987
|
-
user_langchain_project=user_langchain_project,
|
|
988
904
|
project_root=project_root,
|
|
989
905
|
shell_allow_list=shell_allow_list,
|
|
990
906
|
extra_skills_dirs=extra_skills_dirs,
|
|
@@ -994,13 +910,11 @@ class Settings:
|
|
|
994
910
|
"""Reload selected settings from environment variables and project files.
|
|
995
911
|
|
|
996
912
|
This refreshes only fields that are expected to change at runtime
|
|
997
|
-
(API keys, Google Cloud project, project root, shell allow-list
|
|
998
|
-
LangSmith tracing project).
|
|
913
|
+
(API keys, Google Cloud project, project root, and shell allow-list).
|
|
999
914
|
|
|
1000
915
|
Runtime model state (`model_name`, `model_provider`,
|
|
1001
|
-
`model_context_limit`)
|
|
1002
|
-
|
|
1003
|
-
not in `reloadable_fields` and are never touched by this method.
|
|
916
|
+
`model_context_limit`) is intentionally preserved — it is not in
|
|
917
|
+
`reloadable_fields` and is never touched by this method.
|
|
1004
918
|
|
|
1005
919
|
!!! note
|
|
1006
920
|
|
|
@@ -1034,7 +948,6 @@ class Settings:
|
|
|
1034
948
|
"nvidia_api_key",
|
|
1035
949
|
"tavily_api_key",
|
|
1036
950
|
"google_cloud_project",
|
|
1037
|
-
"soothe_langchain_project",
|
|
1038
951
|
"project_root",
|
|
1039
952
|
"shell_allow_list",
|
|
1040
953
|
"extra_skills_dirs",
|
|
@@ -1042,15 +955,14 @@ class Settings:
|
|
|
1042
955
|
"""Fields refreshed on `/reload`.
|
|
1043
956
|
|
|
1044
957
|
Runtime model state (`model_name`, `model_provider`, `model_context_limit`)
|
|
1045
|
-
|
|
1046
|
-
|
|
958
|
+
is intentionally excluded — it is set once and should not change across
|
|
959
|
+
reloads.
|
|
1047
960
|
"""
|
|
1048
961
|
|
|
1049
962
|
previous = {field: getattr(self, field) for field in reloadable_fields}
|
|
1050
963
|
|
|
1051
964
|
from soothe_cli.tui._env_vars import (
|
|
1052
965
|
EXTRA_SKILLS_DIRS,
|
|
1053
|
-
LANGSMITH_PROJECT,
|
|
1054
966
|
SHELL_ALLOW_LIST,
|
|
1055
967
|
)
|
|
1056
968
|
|
|
@@ -1080,7 +992,6 @@ class Settings:
|
|
|
1080
992
|
"nvidia_api_key": resolve_env_var("NVIDIA_API_KEY"),
|
|
1081
993
|
"tavily_api_key": resolve_env_var("TAVILY_API_KEY"),
|
|
1082
994
|
"google_cloud_project": resolve_env_var("GOOGLE_CLOUD_PROJECT"),
|
|
1083
|
-
"soothe_langchain_project": resolve_env_var(LANGSMITH_PROJECT),
|
|
1084
995
|
"project_root": project_root,
|
|
1085
996
|
"shell_allow_list": shell_allow_list,
|
|
1086
997
|
"extra_skills_dirs": _parse_extra_skills_dirs(
|
|
@@ -1092,18 +1003,6 @@ class Settings:
|
|
|
1092
1003
|
for field, value in refreshed.items():
|
|
1093
1004
|
setattr(self, field, value)
|
|
1094
1005
|
|
|
1095
|
-
# Sync the LANGSMITH_PROJECT env var so LangSmith tracing picks up
|
|
1096
|
-
# the change
|
|
1097
|
-
new_project = refreshed["soothe_langchain_project"]
|
|
1098
|
-
if new_project:
|
|
1099
|
-
os.environ["LANGSMITH_PROJECT"] = new_project
|
|
1100
|
-
elif previous["soothe_langchain_project"]:
|
|
1101
|
-
# Override was previously active but new value is unset; restore.
|
|
1102
|
-
if _original_langsmith_project:
|
|
1103
|
-
os.environ["LANGSMITH_PROJECT"] = _original_langsmith_project
|
|
1104
|
-
else:
|
|
1105
|
-
os.environ.pop("LANGSMITH_PROJECT", None)
|
|
1106
|
-
|
|
1107
1006
|
def _display(field: str, value: object) -> str:
|
|
1108
1007
|
if field in api_key_fields:
|
|
1109
1008
|
return "set" if value else "unset"
|
|
@@ -1612,148 +1511,6 @@ def is_shell_command_allowed(command: str, allow_list: list[str] | None) -> bool
|
|
|
1612
1511
|
return found_command
|
|
1613
1512
|
|
|
1614
1513
|
|
|
1615
|
-
def get_langsmith_project_name() -> str | None:
|
|
1616
|
-
"""Resolve the LangSmith project name if tracing is configured.
|
|
1617
|
-
|
|
1618
|
-
Checks for the required API key and tracing environment variables.
|
|
1619
|
-
When both are present, resolves the project name with priority:
|
|
1620
|
-
`settings.soothe_langchain_project` (from
|
|
1621
|
-
`SOOTHE_CLI_LANGSMITH_PROJECT`), then `LANGSMITH_PROJECT` from the
|
|
1622
|
-
environment (note: this may already have been overridden at bootstrap time
|
|
1623
|
-
to match `SOOTHE_CLI_LANGSMITH_PROJECT`), then `'Soothe'`.
|
|
1624
|
-
|
|
1625
|
-
Returns:
|
|
1626
|
-
Project name string when LangSmith tracing is active, None otherwise.
|
|
1627
|
-
"""
|
|
1628
|
-
from soothe_cli.tui.model_config import resolve_env_var
|
|
1629
|
-
|
|
1630
|
-
langsmith_key = resolve_env_var("LANGSMITH_API_KEY") or resolve_env_var("LANGCHAIN_API_KEY")
|
|
1631
|
-
langsmith_tracing = resolve_env_var("LANGSMITH_TRACING") or resolve_env_var(
|
|
1632
|
-
"LANGCHAIN_TRACING_V2"
|
|
1633
|
-
)
|
|
1634
|
-
if not (langsmith_key and langsmith_tracing):
|
|
1635
|
-
return None
|
|
1636
|
-
|
|
1637
|
-
return (
|
|
1638
|
-
_get_settings().soothe_langchain_project or os.environ.get("LANGSMITH_PROJECT") or "Soothe"
|
|
1639
|
-
)
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
def fetch_langsmith_project_url(project_name: str) -> str | None:
|
|
1643
|
-
"""Fetch the LangSmith project URL via the LangSmith client.
|
|
1644
|
-
|
|
1645
|
-
Successful results are cached at module level so repeated calls do not
|
|
1646
|
-
make additional network requests.
|
|
1647
|
-
|
|
1648
|
-
The network call runs in a daemon thread with a hard timeout of
|
|
1649
|
-
`_LANGSMITH_URL_LOOKUP_TIMEOUT_SECONDS`, so this function blocks the
|
|
1650
|
-
calling thread for at most that duration even if LangSmith is unreachable.
|
|
1651
|
-
|
|
1652
|
-
Returns None (with a debug log) on any failure: missing `langsmith` package,
|
|
1653
|
-
network errors, invalid project names, client initialization issues,
|
|
1654
|
-
or timeouts.
|
|
1655
|
-
|
|
1656
|
-
Args:
|
|
1657
|
-
project_name: LangSmith project name to look up.
|
|
1658
|
-
|
|
1659
|
-
Returns:
|
|
1660
|
-
Project URL string if found, None otherwise.
|
|
1661
|
-
"""
|
|
1662
|
-
global _langsmith_url_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
1663
|
-
|
|
1664
|
-
if _langsmith_url_cache is not None:
|
|
1665
|
-
cached_name, cached_url = _langsmith_url_cache
|
|
1666
|
-
if cached_name == project_name:
|
|
1667
|
-
return cached_url
|
|
1668
|
-
# Different project name — fall through to fetch.
|
|
1669
|
-
|
|
1670
|
-
try:
|
|
1671
|
-
from langsmith import Client
|
|
1672
|
-
except ImportError:
|
|
1673
|
-
logger.debug(
|
|
1674
|
-
"Could not fetch LangSmith project URL for '%s'",
|
|
1675
|
-
project_name,
|
|
1676
|
-
exc_info=True,
|
|
1677
|
-
)
|
|
1678
|
-
return None
|
|
1679
|
-
|
|
1680
|
-
result: str | None = None
|
|
1681
|
-
lookup_error: Exception | None = None
|
|
1682
|
-
done = threading.Event()
|
|
1683
|
-
|
|
1684
|
-
def _lookup_url() -> None:
|
|
1685
|
-
nonlocal result, lookup_error
|
|
1686
|
-
try:
|
|
1687
|
-
from soothe_cli.tui.model_config import resolve_env_var
|
|
1688
|
-
|
|
1689
|
-
# Explicit api_key because Client() reads os.environ directly
|
|
1690
|
-
# and doesn't know about the SOOTHE_ prefix.
|
|
1691
|
-
api_key = resolve_env_var("LANGSMITH_API_KEY") or resolve_env_var("LANGCHAIN_API_KEY")
|
|
1692
|
-
project = Client(api_key=api_key).read_project(project_name=project_name)
|
|
1693
|
-
result = project.url or None
|
|
1694
|
-
except Exception as exc: # noqa: BLE001 # LangSmith SDK error types are not stable
|
|
1695
|
-
lookup_error = exc
|
|
1696
|
-
finally:
|
|
1697
|
-
done.set()
|
|
1698
|
-
|
|
1699
|
-
thread = threading.Thread(target=_lookup_url, daemon=True)
|
|
1700
|
-
thread.start()
|
|
1701
|
-
|
|
1702
|
-
if not done.wait(_LANGSMITH_URL_LOOKUP_TIMEOUT_SECONDS):
|
|
1703
|
-
logger.debug(
|
|
1704
|
-
"Timed out fetching LangSmith project URL for '%s' after %.1fs",
|
|
1705
|
-
project_name,
|
|
1706
|
-
_LANGSMITH_URL_LOOKUP_TIMEOUT_SECONDS,
|
|
1707
|
-
)
|
|
1708
|
-
return None
|
|
1709
|
-
|
|
1710
|
-
if lookup_error is not None:
|
|
1711
|
-
logger.debug(
|
|
1712
|
-
"Could not fetch LangSmith project URL for '%s'",
|
|
1713
|
-
project_name,
|
|
1714
|
-
exc_info=(
|
|
1715
|
-
type(lookup_error),
|
|
1716
|
-
lookup_error,
|
|
1717
|
-
lookup_error.__traceback__,
|
|
1718
|
-
),
|
|
1719
|
-
)
|
|
1720
|
-
return None
|
|
1721
|
-
|
|
1722
|
-
if result is not None:
|
|
1723
|
-
_langsmith_url_cache = (project_name, result)
|
|
1724
|
-
return result
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
def build_langsmith_thread_url(thread_id: str) -> str | None:
|
|
1728
|
-
"""Build a full LangSmith thread URL if tracing is configured.
|
|
1729
|
-
|
|
1730
|
-
Combines `get_langsmith_project_name` and `fetch_langsmith_project_url`
|
|
1731
|
-
into a single convenience helper.
|
|
1732
|
-
|
|
1733
|
-
Args:
|
|
1734
|
-
thread_id: Thread identifier to build the URL for.
|
|
1735
|
-
|
|
1736
|
-
Returns:
|
|
1737
|
-
Full thread URL string, or `None` if unavailable (LangSmith is not
|
|
1738
|
-
configured or the project URL cannot be resolved.)
|
|
1739
|
-
"""
|
|
1740
|
-
project_name = get_langsmith_project_name()
|
|
1741
|
-
if not project_name:
|
|
1742
|
-
return None
|
|
1743
|
-
|
|
1744
|
-
project_url = fetch_langsmith_project_url(project_name)
|
|
1745
|
-
if not project_url:
|
|
1746
|
-
return None
|
|
1747
|
-
|
|
1748
|
-
return f"{project_url.rstrip('/')}/t/{thread_id}?utm_source=Soothe"
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
def reset_langsmith_url_cache() -> None:
|
|
1752
|
-
"""Reset the LangSmith URL cache (for testing)."""
|
|
1753
|
-
global _langsmith_url_cache # noqa: PLW0603 # Module-level cache requires global statement
|
|
1754
|
-
_langsmith_url_cache = None
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
1514
|
def get_default_coding_instructions() -> str:
|
|
1758
1515
|
"""Get the default coding agent instructions.
|
|
1759
1516
|
|
|
@@ -171,15 +171,15 @@ def resolve_env_var(var_name: str) -> str:
|
|
|
171
171
|
"""Resolve environment variable with SOOTHE_ prefix support.
|
|
172
172
|
|
|
173
173
|
This function handles two scenarios:
|
|
174
|
-
1. Direct env var lookup: resolve_env_var("
|
|
175
|
-
- First checks
|
|
176
|
-
- Falls back to
|
|
177
|
-
2. Pattern resolution: resolve_env_var("${
|
|
174
|
+
1. Direct env var lookup: resolve_env_var("OPENAI_API_KEY")
|
|
175
|
+
- First checks SOOTHE_OPENAI_API_KEY
|
|
176
|
+
- Falls back to OPENAI_API_KEY
|
|
177
|
+
2. Pattern resolution: resolve_env_var("${OPENAI_API_KEY}")
|
|
178
178
|
- Resolves ${VAR} patterns within strings
|
|
179
179
|
|
|
180
180
|
Args:
|
|
181
|
-
var_name: Environment variable name (e.g., "
|
|
182
|
-
or pattern string (e.g., "${
|
|
181
|
+
var_name: Environment variable name (e.g., "OPENAI_API_KEY")
|
|
182
|
+
or pattern string (e.g., "${OPENAI_API_KEY}")
|
|
183
183
|
|
|
184
184
|
Returns:
|
|
185
185
|
Resolved value from environment, or empty string if not found.
|
|
@@ -2392,13 +2392,21 @@ class CognitionGoalTreeMessage(_TimestampClickMixin, Vertical):
|
|
|
2392
2392
|
self,
|
|
2393
2393
|
*,
|
|
2394
2394
|
status: str,
|
|
2395
|
-
goal_progress: float
|
|
2395
|
+
goal_progress: str, # IG-399: descriptive level instead of float
|
|
2396
2396
|
completion_summary: str,
|
|
2397
2397
|
total_steps: int,
|
|
2398
2398
|
) -> None:
|
|
2399
2399
|
"""Show a compact footer when the agentic loop completes."""
|
|
2400
|
-
|
|
2401
|
-
|
|
2400
|
+
# IG-399: Map descriptive levels to percentage display
|
|
2401
|
+
progress_map = {
|
|
2402
|
+
"none": "0%",
|
|
2403
|
+
"low": "20%",
|
|
2404
|
+
"medium": "50%",
|
|
2405
|
+
"high": "80%",
|
|
2406
|
+
"complete": "100%",
|
|
2407
|
+
}
|
|
2408
|
+
pct_display = progress_map.get(goal_progress, "0%")
|
|
2409
|
+
parts: list[str] = [str(status or "done"), pct_display]
|
|
2402
2410
|
if total_steps:
|
|
2403
2411
|
parts.append(f"{total_steps} step(s)")
|
|
2404
2412
|
cs = (completion_summary or "").strip()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/rendering/async_renderer_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/execution.py
RENAMED
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/fallback.py
RENAMED
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/goal_formatter.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/structured.py
RENAMED
|
File without changes
|
{soothe_cli-0.4.3 → soothe_cli-0.4.5}/src/soothe_cli/shared/tools/tool_formatters/subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|